diff --git a/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj b/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj index 3befd3bde21e..e4d7504610b0 100644 --- a/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj +++ b/src/Controls/samples/Controls.Sample/Maui.Controls.Sample.csproj @@ -78,7 +78,6 @@ - diff --git a/src/Controls/src/Core/HybridWebView/HybridWebView.cs b/src/Controls/src/Core/HybridWebView/HybridWebView.cs index 509b10062a8e..26bd068576c4 100644 --- a/src/Controls/src/Core/HybridWebView/HybridWebView.cs +++ b/src/Controls/src/Core/HybridWebView/HybridWebView.cs @@ -66,6 +66,32 @@ void IHybridWebView.RawMessageReceived(string rawMessage) /// public event EventHandler? RawMessageReceived; + /// + void IInitializationAwareWebView.WebViewInitializationStarted(WebViewInitializationStartedEventArgs args) + { + var platformArgs = new PlatformWebViewInitializingEventArgs(args); + var e = new WebViewInitializingEventArgs(platformArgs); + WebViewInitializing?.Invoke(this, e); + } + + /// + /// Raised when the web view is initializing. This event allows the application to perform additional configuration. + /// + public event EventHandler? WebViewInitializing; + + /// + void IInitializationAwareWebView.WebViewInitializationCompleted(WebViewInitializationCompletedEventArgs args) + { + var platformArgs = new PlatformWebViewInitializedEventArgs(args); + var e = new WebViewInitializedEventArgs(platformArgs); + WebViewInitialized?.Invoke(this, e); + } + + /// + /// Raised when the web view has been initialized. + /// + public event EventHandler? WebViewInitialized; + /// bool IWebRequestInterceptingWebView.WebResourceRequested(WebResourceRequestedEventArgs args) { diff --git a/src/Controls/src/Core/InitializationAwareWebView/PlatformWebViewInitializedEventArgs.cs b/src/Controls/src/Core/InitializationAwareWebView/PlatformWebViewInitializedEventArgs.cs new file mode 100644 index 000000000000..f7df72ee3422 --- /dev/null +++ b/src/Controls/src/Core/InitializationAwareWebView/PlatformWebViewInitializedEventArgs.cs @@ -0,0 +1,119 @@ +#if WINDOWS +using Microsoft.Web.WebView2.Core; +#elif IOS || MACCATALYST +using WebKit; +#elif ANDROID +using Android.Webkit; +#endif + +namespace Microsoft.Maui.Controls; + +/// +/// Provides platform-specific information about the event. +/// +public class PlatformWebViewInitializedEventArgs +{ +#if IOS || MACCATALYST + + /// + /// Initializes a new instance of the class. + /// + /// The native view that is being initialized. + /// The settings for the web view, which can be used to configure various aspects of the web view. + internal PlatformWebViewInitializedEventArgs(WKWebView sender, WKWebViewConfiguration configuration) + { + Sender = sender; + Configuration = configuration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The event arguments containing the native view and configuration. + internal PlatformWebViewInitializedEventArgs(WebViewInitializationCompletedEventArgs args) + : this(args.Sender, args.Configuration) + { + } + + /// + /// Gets the native view attached to the event. + /// + public WKWebView Sender { get; } + + /// + /// Gets or sets the settings attached to the web view. + /// + public WKWebViewConfiguration Configuration { get; } + +#elif ANDROID + + /// + /// Initializes a new instance of the class. + /// + /// The native view that is being initialized. + /// The settings for the web view, which can be used to configure various aspects of the web view. + internal PlatformWebViewInitializedEventArgs(global::Android.Webkit.WebView sender, WebSettings settings) + { + Sender = sender; + Settings = settings; + } + + /// + /// Initializes a new instance of the class. + /// + /// The event arguments containing the native view and configuration. + internal PlatformWebViewInitializedEventArgs(WebViewInitializationCompletedEventArgs args) + : this(args.Sender, args.Settings) + { + } + + /// + /// Gets the native view attached to the event. + /// + public global::Android.Webkit.WebView Sender { get; } + + /// + /// Gets or sets the settings attached to the web view. + /// + public WebSettings Settings { get; } + +#elif WINDOWS + + /// + /// Initializes a new instance of the class. + /// + /// The native view that is being initialized. + /// The settings for the web view, which can be used to configure various aspects of the web view. + internal PlatformWebViewInitializedEventArgs(CoreWebView2 sender, CoreWebView2Settings settings) + { + Sender = sender; + Settings = settings; + } + + /// + /// Initializes a new instance of the class. + /// + /// The event arguments containing the native view and configuration. + internal PlatformWebViewInitializedEventArgs(WebViewInitializationCompletedEventArgs args) + : this(args.Sender, args.Settings) + { + } + + /// + /// Gets the native view attached to the event. + /// + public CoreWebView2 Sender { get; } + + /// + /// Gets or sets the settings attached to the web view. + /// + public CoreWebView2Settings Settings { get; } + +#else + + internal PlatformWebViewInitializedEventArgs(WebViewInitializationCompletedEventArgs args) + { + } + +#endif +} diff --git a/src/Controls/src/Core/InitializationAwareWebView/PlatformWebViewInitializingEventArgs.cs b/src/Controls/src/Core/InitializationAwareWebView/PlatformWebViewInitializingEventArgs.cs new file mode 100644 index 000000000000..2db074613c64 --- /dev/null +++ b/src/Controls/src/Core/InitializationAwareWebView/PlatformWebViewInitializingEventArgs.cs @@ -0,0 +1,131 @@ +#if IOS || MACCATALYST +using WebKit; +#elif ANDROID +using Android.Webkit; +#elif WINDOWS +using Microsoft.Web.WebView2.Core; +#endif + +namespace Microsoft.Maui.Controls; + +/// +/// Provides platform-specific information about the event. +/// +public class PlatformWebViewInitializingEventArgs +{ + readonly WebViewInitializationStartedEventArgs _coreArgs; + + internal PlatformWebViewInitializingEventArgs(WebViewInitializationStartedEventArgs args) + { + _coreArgs = args; + } + +#if WINDOWS + + /// + /// Gets or sets the relative path to the folder that contains a custom + /// version of WebView2 Runtime. + /// + /// + /// To use a fixed version of the WebView2 Runtime, set this property to + /// the folder path that contains the fixed version of the WebView2 Runtime. + /// + public string? BrowserExecutableFolder + { + get => _coreArgs.BrowserExecutableFolder; + set => _coreArgs.BrowserExecutableFolder = value; + } + + /// + /// Gets or sets the user data folder location for WebView2. + /// + /// + /// The default user data folder {Executable File Name}.WebView2 is created + /// in the same directory next to the compiled code for the app. + /// WebView2 creation fails if the compiled code is running in a directory + /// in which the process does not have permission to create a new directory. + /// The app is responsible to clean up the associated user data folder + /// when it is done. + /// + public string? UserDataFolder + { + get => _coreArgs.UserDataFolder; + set => _coreArgs.UserDataFolder = value; + } + + /// + /// Gets or sets the options used to create WebView2 Environment. + /// + /// + /// As a browser process may be shared among WebViews, WebView creation fails + /// if the specified options does not match the options of the WebViews + /// that are currently running in the shared browser process. + /// + public CoreWebView2EnvironmentOptions? EnvironmentOptions + { + get => _coreArgs.EnvironmentOptions; + set => _coreArgs.EnvironmentOptions = value; + } + + /// + /// Gets or sets whether the WebView2 controller is in private mode. + /// + public bool IsInPrivateModeEnabled + { + get => _coreArgs.IsInPrivateModeEnabled; + set => _coreArgs.IsInPrivateModeEnabled = value; + } + + /// + /// Gets or sets the name of the controller profile. + /// + /// + /// Profile names are only allowed to contain the following ASCII characters: + /// * alphabet characters: a-z and A-Z + /// * digit characters: 0-9 + /// * and '#', '@', '$', '(', ')', '+', '-', '_', '~', '.', ' ' (space). + /// It has a maximum length of 64 characters excluding the null-terminator. + /// It is ASCII case insensitive. + /// + public string? ProfileName + { + get => _coreArgs.ProfileName; + set => _coreArgs.ProfileName = value; + } + + /// + /// Gets or sets the controller's default script locale. + /// + /// + /// This property sets the default locale for all Intl JavaScript APIs and other JavaScript APIs that + /// depend on it, namely Intl.DateTimeFormat() which affects string formatting like in the time/date + /// formats. The intended locale value is in the format of BCP 47 Language Tags. + /// More information can be found from https://www.ietf.org/rfc/bcp/bcp47.html. + /// The default value for ScriptLocale will be depend on the WebView2 language and OS region. + /// If the language portions of the WebView2 language and OS region match, then it will use the OS region. + /// Otherwise, it will use the WebView2 language. + /// + public string? ScriptLocale + { + get => _coreArgs.ScriptLocale; + set => _coreArgs.ScriptLocale = value; + } + +#elif IOS || MACCATALYST + + /// + /// Gets or sets the configuration to be used in the construction of the WKWebView instance. + /// + public WKWebViewConfiguration Configuration => _coreArgs.Configuration; + +#elif ANDROID + + /// + /// Gets the platform-specific settings for the WebView. + /// + public WebSettings Settings => _coreArgs.Settings; + +#else + +#endif +} diff --git a/src/Controls/src/Core/InitializationAwareWebView/WebViewInitializedEventArgs.cs b/src/Controls/src/Core/InitializationAwareWebView/WebViewInitializedEventArgs.cs new file mode 100644 index 000000000000..0e9261c08eb3 --- /dev/null +++ b/src/Controls/src/Core/InitializationAwareWebView/WebViewInitializedEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Microsoft.Maui.Controls; + +/// +/// Event arguments for the event. +/// +public class WebViewInitializedEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class + /// with the specified platform-specific arguments. + /// + /// The platform-specific event arguments. + public WebViewInitializedEventArgs(PlatformWebViewInitializedEventArgs platformArgs) + { + PlatformArgs = platformArgs; + } + + /// + /// Gets the platform-specific event arguments. + /// + public PlatformWebViewInitializedEventArgs? PlatformArgs { get; } +} diff --git a/src/Controls/src/Core/InitializationAwareWebView/WebViewInitializingEventArgs.cs b/src/Controls/src/Core/InitializationAwareWebView/WebViewInitializingEventArgs.cs new file mode 100644 index 000000000000..595efc130e21 --- /dev/null +++ b/src/Controls/src/Core/InitializationAwareWebView/WebViewInitializingEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Microsoft.Maui.Controls; + +/// +/// Event arguments for the event. +/// +public class WebViewInitializingEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class + /// with the specified platform-specific arguments. + /// + /// The platform-specific event arguments. + public WebViewInitializingEventArgs(PlatformWebViewInitializingEventArgs platformArgs) + { + PlatformArgs = platformArgs; + } + + /// + /// Gets the platform-specific event arguments. + /// + public PlatformWebViewInitializingEventArgs? PlatformArgs { get; } +} diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index 6d1a77194cde..2defb52d4526 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -76,6 +76,8 @@ Microsoft.Maui.Controls.Handlers.TabbedPageManager.NotifyDataSetChanged() -> voi ~Microsoft.Maui.Controls.Handlers.TabbedPageManager.previousPage -> Microsoft.Maui.Controls.Page Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitialized -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitializing -> System.EventHandler? Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.IExtendedTypeConverter.ConvertFromInvariantString(string! value, System.IServiceProvider! serviceProvider) -> object? @@ -141,6 +143,11 @@ Microsoft.Maui.Controls.PickerClosedEventArgs.PickerClosedEventArgs() -> void Microsoft.Maui.Controls.PickerOpenedEventArgs Microsoft.Maui.Controls.PickerOpenedEventArgs.PickerOpenedEventArgs() -> void Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.Popover = 5 -> Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Sender.get -> Android.Webkit.WebView! +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Settings.get -> Android.Webkit.WebSettings! +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.Settings.get -> Android.Webkit.WebSettings! Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.Request.get -> Android.Webkit.IWebResourceRequest! Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.Response.get -> Android.Webkit.WebResourceResponse? @@ -194,6 +201,12 @@ Microsoft.Maui.Controls.TimePickerClosedEventArgs Microsoft.Maui.Controls.TimePickerClosedEventArgs.TimePickerClosedEventArgs() -> void Microsoft.Maui.Controls.TimePickerOpenedEventArgs Microsoft.Maui.Controls.TimePickerOpenedEventArgs.TimePickerOpenedEventArgs() -> void +Microsoft.Maui.Controls.WebViewInitializedEventArgs +Microsoft.Maui.Controls.WebViewInitializedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs? +Microsoft.Maui.Controls.WebViewInitializedEventArgs.WebViewInitializedEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs! platformArgs) -> void +Microsoft.Maui.Controls.WebViewInitializingEventArgs +Microsoft.Maui.Controls.WebViewInitializingEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs? +Microsoft.Maui.Controls.WebViewInitializingEventArgs.WebViewInitializingEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs! platformArgs) -> void Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.get -> bool Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 89366d6eb3e0..8bf7455b58fa 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -64,6 +64,8 @@ Microsoft.Maui.Controls.Handlers.Items.MauiCollectionView Microsoft.Maui.Controls.Handlers.Items.MauiCollectionView.MauiCollectionView(CoreGraphics.CGRect frame, UIKit.UICollectionViewLayout! layout) -> void Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitialized -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitializing -> System.EventHandler? Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.IExtendedTypeConverter.ConvertFromInvariantString(string! value, System.IServiceProvider! serviceProvider) -> object? @@ -129,6 +131,11 @@ Microsoft.Maui.Controls.PickerClosedEventArgs.PickerClosedEventArgs() -> void Microsoft.Maui.Controls.PickerOpenedEventArgs Microsoft.Maui.Controls.PickerOpenedEventArgs.PickerOpenedEventArgs() -> void Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.Popover = 5 -> Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.Request.get -> Foundation.NSUrlRequest! Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! @@ -179,6 +186,12 @@ Microsoft.Maui.Controls.TimePickerClosedEventArgs Microsoft.Maui.Controls.TimePickerClosedEventArgs.TimePickerClosedEventArgs() -> void Microsoft.Maui.Controls.TimePickerOpenedEventArgs Microsoft.Maui.Controls.TimePickerOpenedEventArgs.TimePickerOpenedEventArgs() -> void +Microsoft.Maui.Controls.WebViewInitializedEventArgs +Microsoft.Maui.Controls.WebViewInitializedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs? +Microsoft.Maui.Controls.WebViewInitializedEventArgs.WebViewInitializedEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs! platformArgs) -> void +Microsoft.Maui.Controls.WebViewInitializingEventArgs +Microsoft.Maui.Controls.WebViewInitializingEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs? +Microsoft.Maui.Controls.WebViewInitializingEventArgs.WebViewInitializingEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs! platformArgs) -> void Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.get -> bool Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 89366d6eb3e0..8bf7455b58fa 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -64,6 +64,8 @@ Microsoft.Maui.Controls.Handlers.Items.MauiCollectionView Microsoft.Maui.Controls.Handlers.Items.MauiCollectionView.MauiCollectionView(CoreGraphics.CGRect frame, UIKit.UICollectionViewLayout! layout) -> void Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitialized -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitializing -> System.EventHandler? Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.IExtendedTypeConverter.ConvertFromInvariantString(string! value, System.IServiceProvider! serviceProvider) -> object? @@ -129,6 +131,11 @@ Microsoft.Maui.Controls.PickerClosedEventArgs.PickerClosedEventArgs() -> void Microsoft.Maui.Controls.PickerOpenedEventArgs Microsoft.Maui.Controls.PickerOpenedEventArgs.PickerOpenedEventArgs() -> void Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.Popover = 5 -> Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.Request.get -> Foundation.NSUrlRequest! Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! @@ -179,6 +186,12 @@ Microsoft.Maui.Controls.TimePickerClosedEventArgs Microsoft.Maui.Controls.TimePickerClosedEventArgs.TimePickerClosedEventArgs() -> void Microsoft.Maui.Controls.TimePickerOpenedEventArgs Microsoft.Maui.Controls.TimePickerOpenedEventArgs.TimePickerOpenedEventArgs() -> void +Microsoft.Maui.Controls.WebViewInitializedEventArgs +Microsoft.Maui.Controls.WebViewInitializedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs? +Microsoft.Maui.Controls.WebViewInitializedEventArgs.WebViewInitializedEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs! platformArgs) -> void +Microsoft.Maui.Controls.WebViewInitializingEventArgs +Microsoft.Maui.Controls.WebViewInitializingEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs? +Microsoft.Maui.Controls.WebViewInitializingEventArgs.WebViewInitializingEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs! platformArgs) -> void Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.get -> bool Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 10072f46d0c0..0fbfb8cf3540 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -62,6 +62,8 @@ Microsoft.Maui.Controls.DatePickerOpenedEventArgs.DatePickerOpenedEventArgs() -> Microsoft.Maui.Controls.FlexLayout.CrossPlatformMeasure(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitialized -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitializing -> System.EventHandler? Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.IExtendedTypeConverter.ConvertFromInvariantString(string! value, System.IServiceProvider! serviceProvider) -> object? @@ -127,6 +129,22 @@ Microsoft.Maui.Controls.PickerClosedEventArgs.PickerClosedEventArgs() -> void Microsoft.Maui.Controls.PickerOpenedEventArgs Microsoft.Maui.Controls.PickerOpenedEventArgs.PickerOpenedEventArgs() -> void Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.Popover = 5 -> Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Sender.get -> Microsoft.Web.WebView2.Core.CoreWebView2! +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs.Settings.get -> Microsoft.Web.WebView2.Core.CoreWebView2Settings! +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.BrowserExecutableFolder.get -> string? +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.BrowserExecutableFolder.set -> void +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.EnvironmentOptions.get -> Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions? +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.EnvironmentOptions.set -> void +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.IsInPrivateModeEnabled.get -> bool +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.IsInPrivateModeEnabled.set -> void +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.ProfileName.get -> string? +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.ProfileName.set -> void +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.ScriptLocale.get -> string? +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.ScriptLocale.set -> void +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.UserDataFolder.get -> string? +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs.UserDataFolder.set -> void Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.Request.get -> Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequest! Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs.RequestEventArgs.get -> Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs! @@ -177,6 +195,12 @@ Microsoft.Maui.Controls.TimePickerClosedEventArgs Microsoft.Maui.Controls.TimePickerClosedEventArgs.TimePickerClosedEventArgs() -> void Microsoft.Maui.Controls.TimePickerOpenedEventArgs Microsoft.Maui.Controls.TimePickerOpenedEventArgs.TimePickerOpenedEventArgs() -> void +Microsoft.Maui.Controls.WebViewInitializedEventArgs +Microsoft.Maui.Controls.WebViewInitializedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs? +Microsoft.Maui.Controls.WebViewInitializedEventArgs.WebViewInitializedEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs! platformArgs) -> void +Microsoft.Maui.Controls.WebViewInitializingEventArgs +Microsoft.Maui.Controls.WebViewInitializingEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs? +Microsoft.Maui.Controls.WebViewInitializingEventArgs.WebViewInitializingEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs! platformArgs) -> void Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.get -> bool Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 54b7a671c78d..112869fc21bf 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -62,6 +62,8 @@ Microsoft.Maui.Controls.DatePickerOpenedEventArgs.DatePickerOpenedEventArgs() -> Microsoft.Maui.Controls.FlexLayout.CrossPlatformMeasure(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitialized -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitializing -> System.EventHandler? Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.IExtendedTypeConverter.ConvertFromInvariantString(string! value, System.IServiceProvider! serviceProvider) -> object? @@ -127,6 +129,8 @@ Microsoft.Maui.Controls.PickerClosedEventArgs.PickerClosedEventArgs() -> void Microsoft.Maui.Controls.PickerOpenedEventArgs Microsoft.Maui.Controls.PickerOpenedEventArgs.PickerOpenedEventArgs() -> void Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.Popover = 5 -> Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.ScrollView.CascadeInputTransparent.get -> bool Microsoft.Maui.Controls.ScrollView.CascadeInputTransparent.set -> void @@ -174,6 +178,12 @@ Microsoft.Maui.Controls.TimePickerClosedEventArgs Microsoft.Maui.Controls.TimePickerClosedEventArgs.TimePickerClosedEventArgs() -> void Microsoft.Maui.Controls.TimePickerOpenedEventArgs Microsoft.Maui.Controls.TimePickerOpenedEventArgs.TimePickerOpenedEventArgs() -> void +Microsoft.Maui.Controls.WebViewInitializedEventArgs +Microsoft.Maui.Controls.WebViewInitializedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs? +Microsoft.Maui.Controls.WebViewInitializedEventArgs.WebViewInitializedEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs! platformArgs) -> void +Microsoft.Maui.Controls.WebViewInitializingEventArgs +Microsoft.Maui.Controls.WebViewInitializingEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs? +Microsoft.Maui.Controls.WebViewInitializingEventArgs.WebViewInitializingEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs! platformArgs) -> void Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.get -> bool Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.set -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 54b7a671c78d..112869fc21bf 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -62,6 +62,8 @@ Microsoft.Maui.Controls.DatePickerOpenedEventArgs.DatePickerOpenedEventArgs() -> Microsoft.Maui.Controls.FlexLayout.CrossPlatformMeasure(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task! Microsoft.Maui.Controls.HybridWebView.WebResourceRequested -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitialized -> System.EventHandler? +Microsoft.Maui.Controls.HybridWebView.WebViewInitializing -> System.EventHandler? Microsoft.Maui.Controls.ICornerElement Microsoft.Maui.Controls.ICornerElement.CornerRadius.get -> Microsoft.Maui.CornerRadius Microsoft.Maui.Controls.IExtendedTypeConverter.ConvertFromInvariantString(string! value, System.IServiceProvider! serviceProvider) -> object? @@ -127,6 +129,8 @@ Microsoft.Maui.Controls.PickerClosedEventArgs.PickerClosedEventArgs() -> void Microsoft.Maui.Controls.PickerOpenedEventArgs Microsoft.Maui.Controls.PickerOpenedEventArgs.PickerOpenedEventArgs() -> void Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.Popover = 5 -> Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle +Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs +Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs Microsoft.Maui.Controls.PlatformWebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.ScrollView.CascadeInputTransparent.get -> bool Microsoft.Maui.Controls.ScrollView.CascadeInputTransparent.set -> void @@ -174,6 +178,12 @@ Microsoft.Maui.Controls.TimePickerClosedEventArgs Microsoft.Maui.Controls.TimePickerClosedEventArgs.TimePickerClosedEventArgs() -> void Microsoft.Maui.Controls.TimePickerOpenedEventArgs Microsoft.Maui.Controls.TimePickerOpenedEventArgs.TimePickerOpenedEventArgs() -> void +Microsoft.Maui.Controls.WebViewInitializedEventArgs +Microsoft.Maui.Controls.WebViewInitializedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs? +Microsoft.Maui.Controls.WebViewInitializedEventArgs.WebViewInitializedEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializedEventArgs! platformArgs) -> void +Microsoft.Maui.Controls.WebViewInitializingEventArgs +Microsoft.Maui.Controls.WebViewInitializingEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs? +Microsoft.Maui.Controls.WebViewInitializingEventArgs.WebViewInitializingEventArgs(Microsoft.Maui.Controls.PlatformWebViewInitializingEventArgs! platformArgs) -> void Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.get -> bool Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs.Handled.set -> void diff --git a/src/Controls/src/Core/WebRequestInterceptingWebView/WebViewWebResourceRequestedEventArgs.cs b/src/Controls/src/Core/WebRequestInterceptingWebView/WebViewWebResourceRequestedEventArgs.cs index c7101a931486..80632c120cc8 100644 --- a/src/Controls/src/Core/WebRequestInterceptingWebView/WebViewWebResourceRequestedEventArgs.cs +++ b/src/Controls/src/Core/WebRequestInterceptingWebView/WebViewWebResourceRequestedEventArgs.cs @@ -9,7 +9,7 @@ namespace Microsoft.Maui.Controls; /// /// Event arguments for the event. /// -public class WebViewWebResourceRequestedEventArgs +public class WebViewWebResourceRequestedEventArgs : EventArgs { IReadOnlyDictionary? _headers; IReadOnlyDictionary? _queryParams; diff --git a/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj b/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj index 9ff6855c35ec..e8980569ae6f 100644 --- a/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj +++ b/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj @@ -34,7 +34,6 @@ - diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs index 6aa7744e3e11..9fb3f59d29cf 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs @@ -29,6 +29,8 @@ public partial class ControlsHandlerTestBase : HandlerTestBase // There's definitely a chance that the code written to manage this process could be improved public const string RunInNewWindowCollection = "Serialize test because it has to add itself to the main window"; + public const string WebViewsCollection = "Webviews sometimes don't allow configuration to vary in the same process"; + protected override MauiAppBuilder ConfigureBuilder(MauiAppBuilder mauiAppBuilder) { mauiAppBuilder.Services.AddSingleton((_) => new ApplicationStub()); diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs deleted file mode 100644 index 01fc3923ada4..000000000000 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs +++ /dev/null @@ -1,1151 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.IO.Pipelines; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Hosting; -using Xunit; - -namespace Microsoft.Maui.DeviceTests -{ - [Category(TestCategory.HybridWebView)] - public partial class HybridWebViewTests : ControlsHandlerTestBase - { - void SetupBuilder() - { - EnsureHandlerCreated(builder => - { - builder.ConfigureMauiHandlers(handlers => - { - handlers.AddHandler(); - }); - - builder.Services.AddHybridWebViewDeveloperTools(); - builder.Services.AddScoped(); - }); - } - - [Fact] - public Task LoadsHtmlAndSendReceiveRawMessage() => - RunTest(async (hybridWebView) => - { - var lastRawMessage = ""; - - hybridWebView.RawMessageReceived += (s, e) => - { - lastRawMessage = e.Message; - }; - - const string TestRawMessage = "Hybrid\"\"'' {Test} with chars!"; - hybridWebView.SendRawMessage(TestRawMessage); - - var passed = false; - - for (var i = 0; i < 10; i++) - { - if (lastRawMessage == "You said: " + TestRawMessage) - { - passed = true; - break; - } - - await Task.Delay(1000); - } - - Assert.True(passed, $"Waited for raw message response but it never arrived or didn't match (last message: {lastRawMessage})"); - }); - - [Theory] - [InlineData("/asyncdata.txt", 200)] - [InlineData("/missingfile.txt", 404)] - public Task RequestFileFromJS(string url, int expectedStatus) => - RunTest(async (hybridWebView) => - { - var result = await hybridWebView.InvokeJavaScriptAsync( - "RequestFileFromJS", - HybridWebViewTestContext.Default.Int32, - [url], - [HybridWebViewTestContext.Default.String]); - - Assert.Equal(expectedStatus, result); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNullsAndComplexResult() => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - var result = await hybridWebView.InvokeJavaScriptAsync( - "AddNumbersWithNulls", - HybridWebViewTestContext.Default.ComputationResult, - [x, null, y, null], - [HybridWebViewTestContext.Default.Decimal, null, HybridWebViewTestContext.Default.Decimal, null]); - - Assert.NotNull(result); - Assert.Equal(777.777m, result.result); - Assert.Equal("AdditionWithNulls", result.operationName); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndDecimalResult() => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndReturn", - HybridWebViewTestContext.Default.Decimal, - [x, y], - [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal]); - - Assert.Equal(777.777m, result); - }); - - [Theory] - [InlineData(-123.456)] - [InlineData(0.0)] - [InlineData(123.456)] - public Task InvokeJavaScriptMethodWithParametersAndDoubleResult(double expected) => - RunTest(async (hybridWebView) => - { - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoParameter", - HybridWebViewTestContext.Default.Double, - [expected], - [HybridWebViewTestContext.Default.Double]); - - Assert.Equal(expected, result); - }); - - [Theory] - [InlineData(null)] - [InlineData(-123.456)] - [InlineData(0.0)] - [InlineData(123.456)] - public Task InvokeJavaScriptMethodWithParametersAndNullableDoubleResult(double? expected) => - RunTest(async (hybridWebView) => - { - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoParameter", - HybridWebViewTestContext.Default.NullableDouble, - [expected], - [HybridWebViewTestContext.Default.NullableDouble]); - - Assert.Equal(expected, result); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNewDoubleResult() => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndReturn", - HybridWebViewTestContext.Default.Double, - [x, y], - [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal]); - - Assert.Equal(777.777, result); - }); - - [Theory] - [InlineData(-123)] - [InlineData(0)] - [InlineData(123)] - public Task InvokeJavaScriptMethodWithParametersAndIntResult(int expected) => - RunTest(async (hybridWebView) => - { - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoParameter", - HybridWebViewTestContext.Default.Int32, - [expected], - [HybridWebViewTestContext.Default.Int32]); - - Assert.Equal(expected, result); - }); - - [Theory] - [InlineData(null)] - [InlineData(-123)] - [InlineData(0)] - [InlineData(123)] - public Task InvokeJavaScriptMethodWithParametersAndNullableIntResult(int? expected) => - RunTest(async (hybridWebView) => - { - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoParameter", - HybridWebViewTestContext.Default.NullableInt32, - [expected], - [HybridWebViewTestContext.Default.NullableInt32]); - - Assert.Equal(expected, result); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNewIntResult() => - RunTest(async (hybridWebView) => - { - var x = 123; - var y = 654; - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndReturn", - HybridWebViewTestContext.Default.Int32, - [x, y], - [HybridWebViewTestContext.Default.Int32, HybridWebViewTestContext.Default.Int32]); - - Assert.Equal(777, result); - }); - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("foo")] - [InlineData("null")] - [InlineData("undefined")] - public Task InvokeJavaScriptMethodWithParametersAndStringResult(string expected) => - RunTest(async (hybridWebView) => - { - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoParameter", - HybridWebViewTestContext.Default.String, - [expected], - [HybridWebViewTestContext.Default.String]); - - Assert.Equal(expected, result); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNewStringResult() => - RunTest(async (hybridWebView) => - { - var x = "abc"; - var y = "def"; - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndStringReturn", - HybridWebViewTestContext.Default.String, - [x, y], - [HybridWebViewTestContext.Default.String, HybridWebViewTestContext.Default.String]); - - Assert.Equal("abcdef", result); - }); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public Task InvokeJavaScriptMethodWithParametersAndBoolResult(bool expected) => - RunTest(async (hybridWebView) => - { - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoParameter", - HybridWebViewTestContext.Default.Boolean, - [expected], - [HybridWebViewTestContext.Default.Boolean]); - - Assert.Equal(expected, result); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndComplexResult() => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - var result = await hybridWebView.InvokeJavaScriptAsync( - "AddNumbers", - HybridWebViewTestContext.Default.ComputationResult, - [x, y], - [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal]); - - Assert.NotNull(result); - Assert.Equal(777.777m, result.result); - Assert.Equal("Addition", result.operationName); - }); - - [Fact] - public Task InvokeAsyncJavaScriptMethodWithParametersAndComplexResult() => - RunTest(async (hybridWebView) => - { - var s1 = "new_key"; - var s2 = "new_value"; - - var result = await hybridWebView.InvokeJavaScriptAsync>( - "EvaluateMeWithParamsAndAsyncReturn", - HybridWebViewTestContext.Default.DictionaryStringString, - [s1, s2], - [HybridWebViewTestContext.Default.String, HybridWebViewTestContext.Default.String]); - - Assert.NotNull(result); - Assert.Equal(3, result.Count); - Assert.Equal("value1", result["key1"]); - Assert.Equal("value2", result["key2"]); - Assert.Equal(s2, result[s1]); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndVoidReturn() => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndVoidReturn", - [x, y], - [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal]); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndVoidReturnGetResult", - HybridWebViewTestContext.Default.Decimal); - - Assert.Equal(777.777m, result); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingObjectReturnMethod() => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - var firstResult = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndVoidReturn", - HybridWebViewTestContext.Default.ComputationResult, - [x, y], - [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal]); - - Assert.Null(firstResult); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndVoidReturnGetResult", - HybridWebViewTestContext.Default.Decimal); - - Assert.Equal(777.777m, result); - }); - - [Fact] - public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingNullReturnMethod() => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - var firstResult = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndVoidReturn", - null, - [x, y], - [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal]); - - Assert.Null(firstResult); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EvaluateMeWithParamsAndVoidReturnGetResult", - HybridWebViewTestContext.Default.Decimal); - - Assert.Equal(777.777m, result); - }); - - [Fact] - public Task EvaluateJavaScriptAndGetResult() => - RunTest(async (hybridWebView) => - { - // Run some JavaScript to call a method and get result - var result1 = await hybridWebView.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndReturn('abc', 'def')"); - Assert.Equal("abcdef", result1); - - // Run some JavaScript to get an arbitrary result by running JavaScript - var result2 = await hybridWebView.EvaluateJavaScriptAsync("window.TestKey"); - Assert.Equal("test_value", result2); - }); - - [Theory] - [ClassData(typeof(InvokeJavaScriptAsyncTestData))] - public Task InvokeDotNet(string methodName, string expectedReturnValue) => - RunTest("invokedotnettests.html", async (hybridWebView) => - { - var invokeJavaScriptTarget = new TestDotNetMethods(); - hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget); - - //await Task.Delay(15_000); - - // Tell JavaScript to invoke the method - hybridWebView.SendRawMessage(methodName); - - // Wait for method invocation to complete - await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView); - - // Run some JavaScript to see if it got the expected result - var result = await hybridWebView.EvaluateJavaScriptAsync("GetLastScriptResult()"); - Assert.Equal(expectedReturnValue, result); - Assert.Equal(methodName, invokeJavaScriptTarget.LastMethodCalled); - }); - - [Theory] - [InlineData("")] - [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsNumber(string type) => - RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 1, ex => - { - Assert.Equal("InvokeJavaScript threw an exception: 777.777", ex.Message); - Assert.Equal("777.777", ex.InnerException.Message); - Assert.Null(ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - }); - - [Theory] - [InlineData("")] - [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsString(string type) => - RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 2, ex => - { - Assert.Equal("InvokeJavaScript threw an exception: String: 777.777", ex.Message); - Assert.Equal("String: 777.777", ex.InnerException.Message); - Assert.Null(ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - }); - - [Theory] - [InlineData("")] - [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsError(string type) => - RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 3, ex => - { - Assert.Equal("InvokeJavaScript threw an exception: Generic Error: 777.777", ex.Message); - Assert.Equal("Generic Error: 777.777", ex.InnerException.Message); - Assert.Equal("Error", ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - }); - - [Theory] - [InlineData("")] - [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) => - RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 4, ex => - { - Assert.Contains("undefined", ex.Message, StringComparison.OrdinalIgnoreCase); - Assert.Contains("undefined", ex.InnerException.Message, StringComparison.OrdinalIgnoreCase); - Assert.Equal("TypeError", ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - }); - - [Fact] - public Task RequestsCanBeInterceptedAndCustomDataReturned() => - RunTest(async (hybridWebView) => - { - hybridWebView.WebResourceRequested += (sender, e) => - { - if (e.Uri.Host == "0.0.0.1") - { - // 1. Create the response data - var response = new EchoResponseObject { message = $"Hello real endpoint (param1={e.QueryParameters["param1"]}, param2={e.QueryParameters["param2"]})" }; - var responseData = JsonSerializer.SerializeToUtf8Bytes(response); - var responseLength = responseData.Length.ToString(CultureInfo.InvariantCulture); - - // 2. Create the response - e.SetResponse(200, "OK", "application/json", new MemoryStream(responseData)); - - // 3. Let the app know we are handling it entirely - e.Handled = true; - } - }; - - var responseObject = await hybridWebView.InvokeJavaScriptAsync( - "RequestsWithAppUriCanBeIntercepted", - HybridWebViewTestContext.Default.EchoResponseObject); - - Assert.NotNull(responseObject); - Assert.Equal("Hello real endpoint (param1=value1, param2=value2)", responseObject.message); - }); - - [Fact] - public Task RequestsCanBeInterceptedAndAsyncCustomDataReturned() => - RunTest(async (hybridWebView) => - { - hybridWebView.WebResourceRequested += (sender, e) => - { - if (e.Uri.Host == "0.0.0.1") - { - // 1. Create the response - e.SetResponse(200, "OK", "application/json", GetDataAsync(e.QueryParameters)); - - // 2. Let the app know we are handling it entirely - e.Handled = true; - } - }; - - var responseObject = await hybridWebView.InvokeJavaScriptAsync( - "RequestsWithAppUriCanBeIntercepted", - HybridWebViewTestContext.Default.EchoResponseObject); - - Assert.NotNull(responseObject); - Assert.Equal("Hello real endpoint (param1=value1, param2=value2)", responseObject.message); - - [RequiresUnreferencedCodeAttribute("Calls System.Text.Json.JsonSerializer.SerializeAsync(Stream, TValue, JsonSerializerOptions, CancellationToken)")] - static async Task GetDataAsync(IReadOnlyDictionary queryParams) - { - var response = new EchoResponseObject { message = $"Hello real endpoint (param1={queryParams["param1"]}, param2={queryParams["param2"]})" }; - - var ms = new MemoryStream(); - - await Task.Delay(1000); - await JsonSerializer.SerializeAsync(ms, response); - await Task.Delay(1000); - - ms.Position = 0; - - return ms; - } - }); - - [Theory] -#if !ANDROID // Custom schemes are not supported on Android -#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 - [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] -#endif -#endif -#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst - [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] -#endif - public Task RequestsCanBeInterceptedAndCustomDataReturnedForDifferentHosts(string uriBase, string function) => - RunTest(async (hybridWebView) => - { - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) - { - return; - } - - hybridWebView.WebResourceRequested += (sender, e) => - { - if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) - { - // 1. Get the request from the platform args - var name = e.Headers["X-Echo-Name"]; - - // 2. Create the response data - var response = new EchoResponseObject - { - message = $"Hello {name} (param1={e.QueryParameters["param1"]}, param2={e.QueryParameters["param2"]})", - }; - var responseData = JsonSerializer.SerializeToUtf8Bytes(response); - var responseLength = responseData.Length.ToString(CultureInfo.InvariantCulture); - - // 3. Create the response - var headers = new Dictionary - { - ["Content-Length"] = responseLength, - ["Access-Control-Allow-Origin"] = "*", - ["Access-Control-Allow-Headers"] = "*", - ["Access-Control-Allow-Methods"] = "GET", - }; - e.SetResponse(200, "OK", headers, new MemoryStream(responseData)); - - // 4. Let the app know we are handling it entirely - e.Handled = true; - } - }; - - var responseObject = await hybridWebView.InvokeJavaScriptAsync( - function, - HybridWebViewTestContext.Default.EchoResponseObject); - - Assert.NotNull(responseObject); - Assert.Equal("Hello Matthew (param1=value1, param2=value2)", responseObject.message); - }); - - [Theory] -#if !ANDROID // Custom schemes are not supported on Android -#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 - [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] -#endif -#endif -#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst - [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] -#endif - public Task RequestsCanBeInterceptedAndHeadersAddedForDifferentHosts(string uriBase, string function) => - RunTest(async (hybridWebView) => - { - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) - { - return; - } - - const string ExpectedHeaderValue = "My Header Value"; - - hybridWebView.WebResourceRequested += (sender, e) => - { - if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) - { -#if WINDOWS - // Add the desired header for Windows by modifying the request - e.PlatformArgs.Request.Headers.SetHeader("X-Request-Header", ExpectedHeaderValue); -#elif IOS || MACCATALYST - // We are going to handle this ourselves - e.Handled = true; - - // Intercept the request and add the desired header to a copy of the request - var task = e.PlatformArgs.UrlSchemeTask; - - // Create a mutable copy of the request (this preserves all existing headers and properties) - var request = e.PlatformArgs.Request.MutableCopy() as Foundation.NSMutableUrlRequest; - - // Set the URL to the desired request URL as iOS only allows us to intercept non-https requests - request.Url = new("https://echo.free.beeceptor.com/sample-request"); - - // Add our custom header - var headers = request.Headers.MutableCopy() as Foundation.NSMutableDictionary; - headers[(Foundation.NSString)"X-Request-Header"] = (Foundation.NSString)ExpectedHeaderValue; - request.Headers = headers; - - // Create a session configuration and session to send the request - var configuration = Foundation.NSUrlSessionConfiguration.DefaultSessionConfiguration; - var session = Foundation.NSUrlSession.FromConfiguration(configuration); - - // Create a data task to send the request and get the response - var dataTask = session.CreateDataTask(request, (data, response, error) => - { - if (error is not null) - { - // Handle the error by completing the task with an error response - task.DidFailWithError(error); - return; - } - - if (response is Foundation.NSHttpUrlResponse httpResponse) - { - // Forward the response headers and status - task.DidReceiveResponse(httpResponse); - - // Forward the response body if any - if (data != null) - { - task.DidReceiveData(data); - } - - // Complete the task - task.DidFinish(); - } - else - { - // Fallback for non-HTTP responses or unexpected response type - task.DidFailWithError(new Foundation.NSError(new Foundation.NSString("HybridWebViewError"), -1, null)); - } - }); - - // Start the request - dataTask.Resume(); -#elif ANDROID - // We are going to handle this ourselves - e.Handled = true; - - // Intercept the request and add the desired header to a new request - var request = e.PlatformArgs.Request; - - // Copy the request - var url = new Java.Net.URL(request.Url.ToString()); - var connection = (Java.Net.HttpURLConnection)url.OpenConnection(); - connection.RequestMethod = request.Method; - foreach (var header in request.RequestHeaders) - { - connection.SetRequestProperty(header.Key, header.Value); - } - - // Add our custom header - connection.SetRequestProperty("X-Request-Header", ExpectedHeaderValue); - - // Set the response property - e.PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse( - connection.ContentType, - connection.ContentEncoding ?? "UTF-8", - (int)connection.ResponseCode, - connection.ResponseMessage, - new Dictionary - { - ["Access-Control-Allow-Origin"] = "*", - ["Access-Control-Allow-Headers"] = "*", - ["Access-Control-Allow-Methods"] = "GET", - }, - connection.InputStream); -#endif - } - }; - - var responseObject = await hybridWebView.InvokeJavaScriptAsync( - function, - HybridWebViewTestContext.Default.ResponseObject); - - Assert.NotNull(responseObject); - Assert.NotNull(responseObject.headers); - Assert.True(responseObject.headers.TryGetValue("X-Request-Header", out var actualHeaderValue)); - Assert.Equal(ExpectedHeaderValue, actualHeaderValue); - }); - - [Theory] -#if !ANDROID // Custom schemes are not supported on Android -#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 - [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] -#endif -#endif -#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst - [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] -#endif - public Task RequestsCanBeInterceptedAndCancelledForDifferentHosts(string uriBase, string function) => - RunTest(async (hybridWebView) => - { - var intercepted = false; - - hybridWebView.WebResourceRequested += (sender, e) => - { - if (new Uri(uriBase).IsBaseOf(e.Uri)) - { - intercepted = true; - - // 1. Create the response - e.SetResponse(403, "Forbidden"); - - // 2. Let the app know we are handling it entirely - e.Handled = true; - } - }; - - await Assert.ThrowsAsync(() => - hybridWebView.InvokeJavaScriptAsync( - function, - HybridWebViewTestContext.Default.ResponseObject)); - - Assert.True(intercepted, "Request was not intercepted"); - }); - - - [Theory] -#if !ANDROID // Custom schemes are not supported on Android -#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 - [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] -#endif -#endif -#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst - [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] -#endif - public Task RequestsCanBeInterceptedAndCaseInsensitiveHeadersRead(string uriBase, string function) => - RunTest(async (hybridWebView) => - { - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) - { - return; - } - - var headerValues = new Dictionary(); - - hybridWebView.WebResourceRequested += (sender, e) => - { - if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) - { - // Should be exactly as set in the JS - try - { headerValues["X-Echo-Name"] = e.Headers["X-Echo-Name"]; } - catch (Exception ex) - { headerValues["X-Echo-Name"] = ex.Message; } - - // Sometimes lowercase is used - try - { headerValues["x-echo-name"] = e.Headers["x-echo-name"]; } - catch (Exception ex) - { headerValues["x-echo-name"] = ex.Message; } - - // This should never actually occur - try - { headerValues["X-ECHO-name"] = e.Headers["X-ECHO-name"]; } - catch (Exception ex) - { headerValues["X-ECHO-name"] = ex.Message; } - - // If the request is for the app:// resources, we return an empty response - // because the tests are not doing anything with the response. - if (e.Uri.Scheme == "app") - { - var headers = new Dictionary - { - ["Content-Type"] = "application/json", - ["Access-Control-Allow-Origin"] = "*", - ["Access-Control-Allow-Headers"] = "*", - ["Access-Control-Allow-Methods"] = "GET", - }; - e.SetResponse(200, "OK", headers, new MemoryStream(Encoding.UTF8.GetBytes("{}"))); - e.Handled = true; - } - } - }; - - var responseObject = await hybridWebView.InvokeJavaScriptAsync( - function, - HybridWebViewTestContext.Default.EchoResponseObject); - - Assert.NotEmpty(headerValues); - Assert.Equal("Matthew", headerValues["X-Echo-Name"]); - Assert.Equal("Matthew", headerValues["x-echo-name"]); - Assert.Equal("Matthew", headerValues["X-ECHO-name"]); - }); - - Task RunExceptionTest(string method, int errorType, Action test) => - RunTest(async (hybridWebView) => - { - var x = 123.456m; - var y = 654.321m; - - var exception = await Assert.ThrowsAnyAsync(() => - hybridWebView.InvokeJavaScriptAsync( - method, - HybridWebViewTestContext.Default.Decimal, - [x, y, errorType], - [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Int32])); - - test(exception); - }); - - Task RunTest(Func test) => - RunTest(null, test); - - async Task RunTest(string defaultFile, Func test) - { - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(24)) - { - return; - } - - SetupBuilder(); - - var hybridWebView = new HybridWebView - { - WidthRequest = 100, - HeightRequest = 100, - - HybridRoot = "HybridTestRoot", - DefaultFile = defaultFile ?? "index.html", - }; - - // Set up the view to be displayed/parented and run our tests on it - await AttachAndRun(hybridWebView, async handler => - { - await WebViewHelpers.WaitForHybridWebViewLoaded(hybridWebView); - - // This is randomly failing on iOS, so let's add a timeout to avoid device tests running for hours - await test(hybridWebView); - }); - } - - private class TestDotNetMethods - { - private static ComputationResult NewComplexResult => - new ComputationResult { result = 123, operationName = "Test" }; - - public string LastMethodCalled { get; private set; } - - public void Invoke_NoParam_NoReturn() - { - UpdateLastMethodCalled(); - } - - public object Invoke_NoParam_ReturnNull() - { - UpdateLastMethodCalled(); - return null; - } - - public async Task Invoke_NoParam_ReturnTask() - { - await Task.Delay(1); - UpdateLastMethodCalled(); - } - - public async Task Invoke_NoParam_ReturnTaskNull() - { - await Task.Delay(1); - UpdateLastMethodCalled(); - return null; - } - - public async Task Invoke_NoParam_ReturnTaskValueType() - { - await Task.Delay(1); - UpdateLastMethodCalled(); - return 2; - } - - public async Task Invoke_NoParam_ReturnTaskComplex() - { - await Task.Delay(1); - UpdateLastMethodCalled(); - return NewComplexResult; - } - - public int Invoke_OneParam_ReturnValueType(Dictionary dict) - { - Assert.NotNull(dict); - Assert.Equal(2, dict.Count); - Assert.Equal(111, dict["first"]); - Assert.Equal(222, dict["second"]); - UpdateLastMethodCalled(); - return dict.Count; - } - - public Dictionary Invoke_OneParam_ReturnDictionary(Dictionary dict) - { - Assert.NotNull(dict); - Assert.Equal(2, dict.Count); - Assert.Equal(111, dict["first"]); - Assert.Equal(222, dict["second"]); - UpdateLastMethodCalled(); - dict["third"] = 333; - return dict; - } - - public ComputationResult Invoke_NullParam_ReturnComplex(object obj) - { - Assert.Null(obj); - UpdateLastMethodCalled(); - return NewComplexResult; - } - - public void Invoke_ManyParams_NoReturn(Dictionary dict, string str, object obj, ComputationResult computationResult, int[] arr) - { - Assert.NotNull(dict); - Assert.Equal(2, dict.Count); - Assert.Equal(111, dict["first"]); - Assert.Equal(222, dict["second"]); - - Assert.Equal("hello", str); - - Assert.Null(obj); - - Assert.NotNull(computationResult); - Assert.Equal("invoke_method", computationResult.operationName); - Assert.Equal(123.456m, computationResult.result, 6); - - Assert.NotNull(arr); - Assert.Equal(2, arr.Length); - Assert.Equal(111, arr[0]); - Assert.Equal(222, arr[1]); - - UpdateLastMethodCalled(); - } - - private void UpdateLastMethodCalled([CallerMemberName] string methodName = null) - { - LastMethodCalled = methodName; - } - } - - private class InvokeJavaScriptAsyncTestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - const string ComplexResult = "{\\\"result\\\":123,\\\"operationName\\\":\\\"Test\\\"}"; - const string DictionaryResult = "{\\\"first\\\":111,\\\"second\\\":222,\\\"third\\\":333}"; - const int ValueTypeResult = 2; - - // Test variations of: - // 1. Data type: ValueType, RefType, string, complex type - // 2. Containers of those types: array, dictionary - // 3. Methods with different return values (none, simple, complex, etc.) - yield return new object[] { "Invoke_NoParam_NoReturn", null }; - yield return new object[] { "Invoke_NoParam_ReturnNull", null }; - yield return new object[] { "Invoke_NoParam_ReturnTask", null }; - yield return new object[] { "Invoke_NoParam_ReturnTaskNull", null }; - yield return new object[] { "Invoke_NoParam_ReturnTaskValueType", ValueTypeResult }; - yield return new object[] { "Invoke_NoParam_ReturnTaskComplex", ComplexResult }; - yield return new object[] { "Invoke_OneParam_ReturnValueType", ValueTypeResult }; - yield return new object[] { "Invoke_OneParam_ReturnDictionary", DictionaryResult }; - yield return new object[] { "Invoke_NullParam_ReturnComplex", ComplexResult }; - yield return new object[] { "Invoke_ManyParams_NoReturn", null }; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - - public class ComputationResult - { - public decimal result { get; set; } - public string operationName { get; set; } - } - - public class ResponseObject - { - public string method { get; set; } - public string protocol { get; set; } - public string host { get; set; } - public string path { get; set; } - public string ip { get; set; } - public Dictionary headers { get; set; } - public Dictionary parsedQueryParams { get; set; } - } - - public class EchoResponseObject - { - public string message { get; set; } - } - - [JsonSourceGenerationOptions(WriteIndented = true)] - [JsonSerializable(typeof(ComputationResult))] - [JsonSerializable(typeof(ResponseObject))] - [JsonSerializable(typeof(EchoResponseObject))] - [JsonSerializable(typeof(int))] - [JsonSerializable(typeof(decimal))] - [JsonSerializable(typeof(bool))] - [JsonSerializable(typeof(int))] - [JsonSerializable(typeof(int?))] - [JsonSerializable(typeof(double))] - [JsonSerializable(typeof(double?))] - [JsonSerializable(typeof(string))] - [JsonSerializable(typeof(Dictionary))] - internal partial class HybridWebViewTestContext : JsonSerializerContext - { - } - - public static partial class WebViewHelpers - { - const int MaxWaitTimes = 100; - const int WaitTimeInMS = 250; - - private static async Task Retry(Func> tryAction, Func> createExceptionWithTimeoutMS) - { - for (var i = 0; i < MaxWaitTimes; i++) - { - if (await tryAction()) - { - await Task.Delay(WaitTimeInMS); - return; - } - - await Task.Delay(WaitTimeInMS); - } - - throw await createExceptionWithTimeoutMS(MaxWaitTimes * WaitTimeInMS); - } - - public static async Task WaitForHybridWebViewLoaded(HybridWebView hybridWebView) - { - await Retry(async () => - { - var loaded = await hybridWebView.EvaluateJavaScriptAsync("('HybridWebView' in window && Object.prototype.hasOwnProperty.call(window, 'HybridWebView')) && (document.getElementById('htmlLoaded') !== null)"); - return loaded == "true"; - }, createExceptionWithTimeoutMS: (int timeoutInMS) => Task.FromResult(new Exception($"Waited {timeoutInMS}ms but couldn't get the HybridWebView test page to be ready."))); - } - - public static async Task WaitForHtmlStatusSet(HybridWebView hybridWebView) - { - await Retry(async () => - { - var controlValue = await hybridWebView.EvaluateJavaScriptAsync("document.getElementById('status').innerText"); - return !string.IsNullOrEmpty(controlValue); - }, createExceptionWithTimeoutMS: (int timeoutInMS) => Task.FromResult(new Exception($"Waited {timeoutInMS}ms but couldn't get status element to have a non-empty value."))); - } - } - - class AsyncStream : Stream - { - readonly Task _streamTask; - Stream _stream; - bool _isDisposed; - - public AsyncStream(Task streamTask) - { - _streamTask = streamTask ?? throw new ArgumentNullException(nameof(streamTask)); - } - - async Task GetStreamAsync(CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_isDisposed, nameof(AsyncStream)); - - if (_stream != null) - return _stream; - - _stream = await _streamTask.ConfigureAwait(false); - return _stream; - } - - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - var stream = await GetStreamAsync(cancellationToken).ConfigureAwait(false); - return await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - } - - public override int Read(byte[] buffer, int offset, int count) - { - var stream = GetStreamAsync().GetAwaiter().GetResult(); - return stream.Read(buffer, offset, count); - } - - public override void Flush() => throw new NotSupportedException(); - - public override Task FlushAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); - - public override bool CanRead => !_isDisposed; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => throw new NotSupportedException(); - - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - - public override void SetLength(long value) => throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - - protected override void Dispose(bool disposing) - { - if (_isDisposed) - return; - - if (disposing) - _stream?.Dispose(); - - _isDisposed = true; - base.Dispose(disposing); - } - - public override async ValueTask DisposeAsync() - { - if (_isDisposed) - return; - - if (_stream != null) - await _stream.DisposeAsync().ConfigureAwait(false); - - _isDisposed = true; - await base.DisposeAsync().ConfigureAwait(false); - } - } - } -} diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs new file mode 100644 index 000000000000..f5c3e3609b37 --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs @@ -0,0 +1,187 @@ +#nullable enable +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; + +namespace Microsoft.Maui.DeviceTests; + +[Category(TestCategory.HybridWebView)] +public partial class HybridWebViewTestsBase : ControlsHandlerTestBase +{ + void SetupBuilder() + { + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + }); + + builder.Services.AddHybridWebViewDeveloperTools(); + builder.Services.AddScoped(); + }); + } + + protected Task RunTest(Func test) => + RunTest(null, test); + + protected async Task RunTest(string? defaultFile, Func test) + { + var hybridWebView = new HybridWebView + { + WidthRequest = 100, + HeightRequest = 100, + + HybridRoot = "HybridTestRoot", + DefaultFile = defaultFile ?? "index.html", + }; + await RunTest(hybridWebView, (handler, view) => test(view)); + } + + protected async Task RunTest(HybridWebView hybridWebView, Func test) + { + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(24)) + { + return; + } + + SetupBuilder(); + + // Set up the view to be displayed/parented and run our tests on it + await AttachAndRun(hybridWebView, async handler => + { + await WebViewHelpers.WaitForHybridWebViewLoaded(hybridWebView); + + await test((HybridWebViewHandler)handler, hybridWebView); + }); + } + + protected static partial class WebViewHelpers + { + const int MaxWaitTimes = 100; + const int WaitTimeInMS = 250; + + private static async Task Retry(Func> tryAction, Func> createExceptionWithTimeoutMS) + { + for (var i = 0; i < MaxWaitTimes; i++) + { + if (await tryAction()) + { + await Task.Delay(WaitTimeInMS); + return; + } + + await Task.Delay(WaitTimeInMS); + } + + throw await createExceptionWithTimeoutMS(MaxWaitTimes * WaitTimeInMS); + } + + public static async Task WaitForHybridWebViewLoaded(HybridWebView hybridWebView) + { + await Retry(async () => + { + var loaded = await hybridWebView.EvaluateJavaScriptAsync("('HybridWebView' in window && Object.prototype.hasOwnProperty.call(window, 'HybridWebView')) && (document.getElementById('htmlLoaded') !== null)"); + return loaded == "true"; + }, createExceptionWithTimeoutMS: (int timeoutInMS) => Task.FromResult(new Exception($"Waited {timeoutInMS}ms but couldn't get the HybridWebView test page to be ready."))); + } + + public static async Task WaitForHtmlStatusSet(HybridWebView hybridWebView) + { + await Retry(async () => + { + var controlValue = await hybridWebView.EvaluateJavaScriptAsync("document.getElementById('status').innerText"); + return !string.IsNullOrEmpty(controlValue); + }, createExceptionWithTimeoutMS: (int timeoutInMS) => Task.FromResult(new Exception($"Waited {timeoutInMS}ms but couldn't get status element to have a non-empty value."))); + } + } + + protected class AsyncStream : Stream + { + readonly Task _streamTask; + Stream? _stream; + bool _isDisposed; + + public AsyncStream(Task streamTask) + { + _streamTask = streamTask ?? throw new ArgumentNullException(nameof(streamTask)); + } + + async Task GetStreamAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_isDisposed, nameof(AsyncStream)); + + if (_stream != null) + return _stream; + + _stream = await _streamTask.ConfigureAwait(false); + return _stream; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var stream = await GetStreamAsync(cancellationToken).ConfigureAwait(false); + return await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var stream = GetStreamAsync().GetAwaiter().GetResult(); + return stream.Read(buffer, offset, count); + } + + public override void Flush() => throw new NotSupportedException(); + + public override Task FlushAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); + + public override bool CanRead => !_isDisposed; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + _stream?.Dispose(); + + _isDisposed = true; + base.Dispose(disposing); + } + + public override async ValueTask DisposeAsync() + { + if (_isDisposed) + return; + + if (_stream != null) + await _stream.DisposeAsync().ConfigureAwait(false); + + _isDisposed = true; + await base.DisposeAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs new file mode 100644 index 000000000000..fcd6b52346cf --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Maui.DeviceTests; + +[Category(TestCategory.HybridWebView)] +#if WINDOWS +[Collection(WebViewsCollection)] +#endif +public partial class HybridWebViewTests_EvaluateJavaScriptAsync : HybridWebViewTestsBase +{ + [Fact] + public Task EvaluateJavaScriptAndGetResult() => + RunTest(async (hybridWebView) => + { + // Run some JavaScript to call a method and get result + var result1 = await hybridWebView.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndReturn('abc', 'def')"); + Assert.Equal("abcdef", result1); + + // Run some JavaScript to get an arbitrary result by running JavaScript + var result2 = await hybridWebView.EvaluateJavaScriptAsync("window.TestKey"); + Assert.Equal("test_value", result2); + }); +} diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_Initialization.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_Initialization.cs new file mode 100644 index 000000000000..9ecc6f9ad3bd --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_Initialization.cs @@ -0,0 +1,214 @@ +#nullable enable +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Handlers; +using Xunit; + +namespace Microsoft.Maui.DeviceTests; + +[Category(TestCategory.HybridWebView)] +#if WINDOWS +[Collection(WebViewsCollection)] +#endif +public partial class HybridWebViewTests_Initialization : HybridWebViewTestsBase +{ + const string UserAgent = "HybridWebViewTests User Agent"; + const string ProfileName = "HybridWebViewTests Test Profile"; + + protected async Task RunTest(Action setup, Action? test = null) + { + var hybridWebView = new HybridWebView + { + WidthRequest = 100, + HeightRequest = 100, + + HybridRoot = "HybridTestRoot", + DefaultFile = "index.html", + }; + + setup(hybridWebView); + + await RunTest(hybridWebView, async (handler, view) => + { + // await just so the HybridWebView can be created and the initialization events can be fired + await Task.Delay(1); + + // Run the actual test + test?.Invoke(handler, hybridWebView); + }); + } + + [Fact] + public async Task InitializingEventIsRaised() + { + var calledCount = 0; + + await RunTest( + hybridWebView => + { + hybridWebView.WebViewInitializing += (s, e) => + { + calledCount++; + + Assert.NotNull(e.PlatformArgs); + +#if IOS || MACCATALYST + Assert.NotNull(e.PlatformArgs.Configuration); +#elif ANDROID + Assert.NotNull(e.PlatformArgs.Settings); +#elif WINDOWS + // Windows does not have a object to configure, but rather a set of properties for each setting +#endif + }; + + }, + (handler, view) => + { + Assert.Equal(1, calledCount); + }); + } + + [Fact] + public async Task InitializedEventIsRaised() + { + var calledCount = 0; + + await RunTest( + hybridWebView => + { + hybridWebView.WebViewInitialized += (s, e) => + { + calledCount++; + + Assert.NotNull(e.PlatformArgs); + Assert.NotNull(e.PlatformArgs.Sender); +#if IOS || MACCATALYST + Assert.NotNull(e.PlatformArgs.Configuration); +#elif ANDROID || WINDOWS + Assert.NotNull(e.PlatformArgs.Settings); +#endif + }; + }, + (handler, view) => + { + Assert.Equal(1, calledCount); + }); + } + + [Fact] + public async Task InitializingEventIsRaisedAndPropertiesSetAreApplied() + { + var calledCount = 0; + + await RunTest( + hybridWebView => + { + hybridWebView.WebViewInitializing += (s, e) => + { + calledCount++; + + Assert.NotNull(e.PlatformArgs); + +#if IOS || MACCATALYST + Assert.NotNull(e.PlatformArgs.Configuration); + e.PlatformArgs.Configuration.ApplicationNameForUserAgent = UserAgent; +#elif ANDROID + Assert.NotNull(e.PlatformArgs.Settings); + e.PlatformArgs.Settings.UserAgentString = UserAgent; +#elif WINDOWS + e.PlatformArgs.ProfileName = ProfileName; +#endif + }; + }, + (handler, view) => + { +#if IOS || MACCATALYST + var actual = handler.PlatformView.Configuration.ApplicationNameForUserAgent; + Assert.Equal(UserAgent, actual); +#elif ANDROID + var actual = handler.PlatformView.Settings.UserAgentString; + Assert.Equal(UserAgent, actual); +#elif WINDOWS + var actual = handler.PlatformView.CoreWebView2.Profile.ProfileName; + Assert.Equal(ProfileName, actual); +#endif + + Assert.Equal(1, calledCount); + }); + } + + [Fact] + public Task CanSetUserAgentUsingProperties() => + RunTest( + hybridWebView => + { + hybridWebView.WebViewInitializing += (s, e) => + { + Assert.NotNull(e.PlatformArgs); + +#if IOS || MACCATALYST + e.PlatformArgs.Configuration.ApplicationNameForUserAgent = UserAgent; +#elif ANDROID + e.PlatformArgs.Settings.UserAgentString = UserAgent; +#endif + }; + hybridWebView.WebViewInitialized += (s, e) => + { + Assert.NotNull(e.PlatformArgs); + +#if WINDOWS + e.PlatformArgs.Settings.UserAgent = UserAgent; +#endif + }; + }, + (handler, view) => + { +#if IOS || MACCATALYST + var actual = handler.PlatformView.Configuration.ApplicationNameForUserAgent; +#elif ANDROID + var actual = handler.PlatformView.Settings.UserAgentString; +#elif WINDOWS + var actual = handler.PlatformView.CoreWebView2.Settings.UserAgent; +#endif + Assert.Equal(UserAgent, actual); + }); + + [Fact] + public Task CanSetUserAgentUsingInitializingEvent() => + RunTest( + hybridWebView => + { + hybridWebView.WebViewInitializing += (s, e) => + { + Assert.NotNull(e.PlatformArgs); + +#if IOS || MACCATALYST + e.PlatformArgs.Configuration.ApplicationNameForUserAgent = UserAgent; +#elif ANDROID + e.PlatformArgs.Settings.UserAgentString = UserAgent; +#elif WINDOWS + // WebView2 requires that different environments have different UDF to support multiple simultaneous instances. + var lad = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + e.PlatformArgs.UserDataFolder = Path.Combine(lad, "Microsoft.Maui.Controls.DeviceTests", $"UserDataFolder-{CanSetUserAgentUsingInitializingEvent}"); + + e.PlatformArgs.EnvironmentOptions = new Web.WebView2.Core.CoreWebView2EnvironmentOptions + { + AdditionalBrowserArguments = $"--user-agent=\"{UserAgent}\"" + }; +#endif + }; + }, + (handler, view) => + { +#if IOS || MACCATALYST + var actual = handler.PlatformView.Configuration.ApplicationNameForUserAgent; +#elif ANDROID + var actual = handler.PlatformView.Settings.UserAgentString; +#elif WINDOWS + var actual = handler.PlatformView.CoreWebView2.Settings.UserAgent; +#endif + Assert.Equal(UserAgent, actual); + }); +} diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_Interception.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_Interception.cs new file mode 100644 index 000000000000..2f91ecec8a30 --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_Interception.cs @@ -0,0 +1,414 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Maui.Handlers; +using Xunit; + +namespace Microsoft.Maui.DeviceTests; + +[Category(TestCategory.HybridWebView)] +#if WINDOWS +[Collection(WebViewsCollection)] +#endif +public partial class HybridWebViewTests_Interception : HybridWebViewTestsBase +{ + [Fact] + public Task RequestsCanBeInterceptedAndCustomDataReturned() => + RunTest(async (hybridWebView) => + { + hybridWebView.WebResourceRequested += (sender, e) => + { + if (e.Uri.Host == "0.0.0.1") + { + // 1. Create the response data + var response = new EchoResponseObject { message = $"Hello real endpoint (param1={e.QueryParameters["param1"]}, param2={e.QueryParameters["param2"]})" }; + var responseData = JsonSerializer.SerializeToUtf8Bytes(response); + var responseLength = responseData.Length.ToString(CultureInfo.InvariantCulture); + + // 2. Create the response + e.SetResponse(200, "OK", "application/json", new MemoryStream(responseData)); + + // 3. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + "RequestsWithAppUriCanBeIntercepted", + InterceptionJsonContext.Default.EchoResponseObject); + + Assert.NotNull(responseObject); + Assert.Equal("Hello real endpoint (param1=value1, param2=value2)", responseObject.message); + }); + + [Fact] + public Task RequestsCanBeInterceptedAndAsyncCustomDataReturned() => + RunTest(async (hybridWebView) => + { + hybridWebView.WebResourceRequested += (sender, e) => + { + if (e.Uri.Host == "0.0.0.1") + { + // 1. Create the response + e.SetResponse(200, "OK", "application/json", GetDataAsync(e.QueryParameters)); + + // 2. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + "RequestsWithAppUriCanBeIntercepted", + InterceptionJsonContext.Default.EchoResponseObject); + + Assert.NotNull(responseObject); + Assert.Equal("Hello real endpoint (param1=value1, param2=value2)", responseObject.message); + + static async Task GetDataAsync(IReadOnlyDictionary queryParams) + { + var response = new EchoResponseObject { message = $"Hello real endpoint (param1={queryParams["param1"]}, param2={queryParams["param2"]})" }; + + var ms = new MemoryStream(); + + await Task.Delay(1000); + await JsonSerializer.SerializeAsync(ms, response); + await Task.Delay(1000); + + ms.Position = 0; + + return ms; + } + }); + + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndCustomDataReturnedForDifferentHosts(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) + { + return; + } + + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + // 1. Get the request from the platform args + var name = e.Headers["X-Echo-Name"]; + + // 2. Create the response data + var response = new EchoResponseObject + { + message = $"Hello {name} (param1={e.QueryParameters["param1"]}, param2={e.QueryParameters["param2"]})", + }; + var responseData = JsonSerializer.SerializeToUtf8Bytes(response); + var responseLength = responseData.Length.ToString(CultureInfo.InvariantCulture); + + // 3. Create the response + var headers = new Dictionary + { + ["Content-Length"] = responseLength, + ["Access-Control-Allow-Origin"] = "*", + ["Access-Control-Allow-Headers"] = "*", + ["Access-Control-Allow-Methods"] = "GET", + }; + e.SetResponse(200, "OK", headers, new MemoryStream(responseData)); + + // 4. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + function, + InterceptionJsonContext.Default.EchoResponseObject); + + Assert.NotNull(responseObject); + Assert.Equal("Hello Matthew (param1=value1, param2=value2)", responseObject.message); + }); + + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndHeadersAddedForDifferentHosts(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) + { + return; + } + + const string ExpectedHeaderValue = "My Header Value"; + + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + Assert.NotNull(e.PlatformArgs); + Assert.NotNull(e.PlatformArgs.Request); + +#if WINDOWS + // Add the desired header for Windows by modifying the request + e.PlatformArgs.Request.Headers.SetHeader("X-Request-Header", ExpectedHeaderValue); +#elif IOS || MACCATALYST + // We are going to handle this ourselves + e.Handled = true; + + // Intercept the request and add the desired header to a copy of the request + var task = e.PlatformArgs.UrlSchemeTask; + + // Create a mutable copy of the request (this preserves all existing headers and properties) + var request = (Foundation.NSMutableUrlRequest)e.PlatformArgs.Request.MutableCopy(); + + // Set the URL to the desired request URL as iOS only allows us to intercept non-https requests + request.Url = new("https://echo.free.beeceptor.com/sample-request"); + + // Add our custom header + Assert.NotNull(request.Headers); + var headers = (Foundation.NSMutableDictionary)request.Headers.MutableCopy(); + headers[(Foundation.NSString)"X-Request-Header"] = (Foundation.NSString)ExpectedHeaderValue; + request.Headers = headers; + + // Create a session configuration and session to send the request + var configuration = Foundation.NSUrlSessionConfiguration.DefaultSessionConfiguration; + var session = Foundation.NSUrlSession.FromConfiguration(configuration); + + // Create a data task to send the request and get the response + var dataTask = session.CreateDataTask(request, (data, response, error) => + { + if (error is not null) + { + // Handle the error by completing the task with an error response + task.DidFailWithError(error); + return; + } + + if (response is Foundation.NSHttpUrlResponse httpResponse) + { + // Forward the response headers and status + task.DidReceiveResponse(httpResponse); + + // Forward the response body if any + if (data != null) + { + task.DidReceiveData(data); + } + + // Complete the task + task.DidFinish(); + } + else + { + // Fallback for non-HTTP responses or unexpected response type + task.DidFailWithError(new Foundation.NSError(new Foundation.NSString("HybridWebViewError"), -1, null)); + } + }); + + // Start the request + dataTask.Resume(); +#elif ANDROID + // We are going to handle this ourselves + e.Handled = true; + + // Intercept the request and add the desired header to a new request + var request = e.PlatformArgs.Request; + + // Copy the request + var url = new Java.Net.URL(request.Url!.ToString()); + var connection = (Java.Net.HttpURLConnection)url.OpenConnection()!; + connection.RequestMethod = request.Method; + foreach (var header in request.RequestHeaders!) + { + connection.SetRequestProperty(header.Key, header.Value); + } + + // Add our custom header + connection.SetRequestProperty("X-Request-Header", ExpectedHeaderValue); + + // Set the response property + e.PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse( + connection.ContentType, + connection.ContentEncoding ?? "UTF-8", + (int)connection.ResponseCode, + connection.ResponseMessage ?? connection.ResponseCode.ToString(), + new Dictionary + { + ["Access-Control-Allow-Origin"] = "*", + ["Access-Control-Allow-Headers"] = "*", + ["Access-Control-Allow-Methods"] = "GET", + }, + connection.InputStream); +#endif + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + function, + InterceptionJsonContext.Default.ResponseObject); + + Assert.NotNull(responseObject); + Assert.NotNull(responseObject.headers); + Assert.True(responseObject.headers.TryGetValue("X-Request-Header", out var actualHeaderValue)); + Assert.Equal(ExpectedHeaderValue, actualHeaderValue); + }); + + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndCancelledForDifferentHosts(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + var intercepted = false; + + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri)) + { + intercepted = true; + + // 1. Create the response + e.SetResponse(403, "Forbidden"); + + // 2. Let the app know we are handling it entirely + e.Handled = true; + } + }; + + await Assert.ThrowsAsync(() => + hybridWebView.InvokeJavaScriptAsync( + function, + InterceptionJsonContext.Default.ResponseObject)); + + Assert.True(intercepted, "Request was not intercepted"); + }); + + + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndCaseInsensitiveHeadersRead(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) + { + return; + } + + var headerValues = new Dictionary(); + + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + // Should be exactly as set in the JS + try + { headerValues["X-Echo-Name"] = e.Headers["X-Echo-Name"]; } + catch (Exception ex) + { headerValues["X-Echo-Name"] = ex.Message; } + + // Sometimes lowercase is used + try + { headerValues["x-echo-name"] = e.Headers["x-echo-name"]; } + catch (Exception ex) + { headerValues["x-echo-name"] = ex.Message; } + + // This should never actually occur + try + { headerValues["X-ECHO-name"] = e.Headers["X-ECHO-name"]; } + catch (Exception ex) + { headerValues["X-ECHO-name"] = ex.Message; } + + // If the request is for the app:// resources, we return an empty response + // because the tests are not doing anything with the response. + if (e.Uri.Scheme == "app") + { + var headers = new Dictionary + { + ["Content-Type"] = "application/json", + ["Access-Control-Allow-Origin"] = "*", + ["Access-Control-Allow-Headers"] = "*", + ["Access-Control-Allow-Methods"] = "GET", + }; + e.SetResponse(200, "OK", headers, new MemoryStream(Encoding.UTF8.GetBytes("{}"))); + e.Handled = true; + } + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + function, + InterceptionJsonContext.Default.EchoResponseObject); + + Assert.NotEmpty(headerValues); + Assert.Equal("Matthew", headerValues["X-Echo-Name"]); + Assert.Equal("Matthew", headerValues["x-echo-name"]); + Assert.Equal("Matthew", headerValues["X-ECHO-name"]); + }); + + public class EchoResponseObject + { + public string? message { get; set; } + } + + public class ResponseObject + { + public string? method { get; set; } + public string? protocol { get; set; } + public string? host { get; set; } + public string? path { get; set; } + public string? ip { get; set; } + public Dictionary? headers { get; set; } + public Dictionary? parsedQueryParams { get; set; } + } + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ResponseObject))] + [JsonSerializable(typeof(EchoResponseObject))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(decimal))] + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(int?))] + [JsonSerializable(typeof(double))] + [JsonSerializable(typeof(double?))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(Dictionary))] + internal partial class InterceptionJsonContext : JsonSerializerContext + { + } +} diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs new file mode 100644 index 000000000000..1d0b36a7faad --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs @@ -0,0 +1,397 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Maui.DeviceTests; + +[Category(TestCategory.HybridWebView)] +#if WINDOWS +[Collection(WebViewsCollection)] +#endif +public partial class HybridWebViewTests_InvokeJavaScriptAsync : HybridWebViewTestsBase +{ + [Theory] + [InlineData("/asyncdata.txt", 200)] + [InlineData("/missingfile.txt", 404)] + public Task RequestFileFromJS(string url, int expectedStatus) => + RunTest(async (hybridWebView) => + { + var result = await hybridWebView.InvokeJavaScriptAsync( + "RequestFileFromJS", + InvokeJsonContext.Default.Int32, + [url], + [InvokeJsonContext.Default.String]); + + Assert.Equal(expectedStatus, result); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndNullsAndComplexResult() => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + var result = await hybridWebView.InvokeJavaScriptAsync( + "AddNumbersWithNulls", + InvokeJsonContext.Default.ComputationResult, + [x, null, y, null], + [InvokeJsonContext.Default.Decimal, null, InvokeJsonContext.Default.Decimal, null]); + + Assert.NotNull(result); + Assert.Equal(777.777m, result.result); + Assert.Equal("AdditionWithNulls", result.operationName); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndDecimalResult() => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + var result = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndReturn", + InvokeJsonContext.Default.Decimal, + [x, y], + [InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Decimal]); + + Assert.Equal(777.777m, result); + }); + + [Theory] + [InlineData(-123.456)] + [InlineData(0.0)] + [InlineData(123.456)] + public Task InvokeJavaScriptMethodWithParametersAndDoubleResult(double expected) => + RunTest(async (hybridWebView) => + { + var result = await hybridWebView.InvokeJavaScriptAsync( + "EchoParameter", + InvokeJsonContext.Default.Double, + [expected], + [InvokeJsonContext.Default.Double]); + + Assert.Equal(expected, result); + }); + + [Theory] + [InlineData(null!)] + [InlineData(-123.456)] + [InlineData(0.0)] + [InlineData(123.456)] + public Task InvokeJavaScriptMethodWithParametersAndNullableDoubleResult(double? expected) => + RunTest(async (hybridWebView) => + { + var result = await hybridWebView.InvokeJavaScriptAsync( + "EchoParameter", + InvokeJsonContext.Default.NullableDouble, + [expected], + [InvokeJsonContext.Default.NullableDouble]); + + Assert.Equal(expected, result); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndNewDoubleResult() => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + var result = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndReturn", + InvokeJsonContext.Default.Double, + [x, y], + [InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Decimal]); + + Assert.Equal(777.777, result); + }); + + [Theory] + [InlineData(-123)] + [InlineData(0)] + [InlineData(123)] + public Task InvokeJavaScriptMethodWithParametersAndIntResult(int expected) => + RunTest(async (hybridWebView) => + { + var result = await hybridWebView.InvokeJavaScriptAsync( + "EchoParameter", + InvokeJsonContext.Default.Int32, + [expected], + [InvokeJsonContext.Default.Int32]); + + Assert.Equal(expected, result); + }); + + [Theory] + [InlineData(null!)] + [InlineData(-123)] + [InlineData(0)] + [InlineData(123)] + public Task InvokeJavaScriptMethodWithParametersAndNullableIntResult(int? expected) => + RunTest(async (hybridWebView) => + { + var result = await hybridWebView.InvokeJavaScriptAsync( + "EchoParameter", + InvokeJsonContext.Default.NullableInt32, + [expected], + [InvokeJsonContext.Default.NullableInt32]); + + Assert.Equal(expected, result); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndNewIntResult() => + RunTest(async (hybridWebView) => + { + var x = 123; + var y = 654; + + var result = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndReturn", + InvokeJsonContext.Default.Int32, + [x, y], + [InvokeJsonContext.Default.Int32, InvokeJsonContext.Default.Int32]); + + Assert.Equal(777, result); + }); + + [Theory] + [InlineData(null!)] + [InlineData("")] + [InlineData("foo")] + [InlineData("null")] + [InlineData("undefined")] + public Task InvokeJavaScriptMethodWithParametersAndStringResult(string? expected) => + RunTest(async (hybridWebView) => + { + var result = await hybridWebView.InvokeJavaScriptAsync( + "EchoParameter", + InvokeJsonContext.Default.String, + [expected], + [InvokeJsonContext.Default.String]); + + Assert.Equal(expected, result); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndNewStringResult() => + RunTest(async (hybridWebView) => + { + var x = "abc"; + var y = "def"; + + var result = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndStringReturn", + InvokeJsonContext.Default.String, + [x, y], + [InvokeJsonContext.Default.String, InvokeJsonContext.Default.String]); + + Assert.Equal("abcdef", result); + }); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public Task InvokeJavaScriptMethodWithParametersAndBoolResult(bool expected) => + RunTest(async (hybridWebView) => + { + var result = await hybridWebView.InvokeJavaScriptAsync( + "EchoParameter", + InvokeJsonContext.Default.Boolean, + [expected], + [InvokeJsonContext.Default.Boolean]); + + Assert.Equal(expected, result); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndComplexResult() => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + var result = await hybridWebView.InvokeJavaScriptAsync( + "AddNumbers", + InvokeJsonContext.Default.ComputationResult, + [x, y], + [InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Decimal]); + + Assert.NotNull(result); + Assert.Equal(777.777m, result.result); + Assert.Equal("Addition", result.operationName); + }); + + [Fact] + public Task InvokeAsyncJavaScriptMethodWithParametersAndComplexResult() => + RunTest(async (hybridWebView) => + { + var s1 = "new_key"; + var s2 = "new_value"; + + var result = await hybridWebView.InvokeJavaScriptAsync>( + "EvaluateMeWithParamsAndAsyncReturn", + InvokeJsonContext.Default.DictionaryStringString, + [s1, s2], + [InvokeJsonContext.Default.String, InvokeJsonContext.Default.String]); + + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("value1", result["key1"]); + Assert.Equal("value2", result["key2"]); + Assert.Equal(s2, result[s1]); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndVoidReturn() => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndVoidReturn", + [x, y], + [InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Decimal]); + + var result = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndVoidReturnGetResult", + InvokeJsonContext.Default.Decimal); + + Assert.Equal(777.777m, result); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingObjectReturnMethod() => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + var firstResult = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndVoidReturn", + InvokeJsonContext.Default.ComputationResult, + [x, y], + [InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Decimal]); + + Assert.Null(firstResult); + + var result = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndVoidReturnGetResult", + InvokeJsonContext.Default.Decimal); + + Assert.Equal(777.777m, result); + }); + + [Fact] + public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingNullReturnMethod() => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + var firstResult = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndVoidReturn", + null!, // secret nullable type to indicate no return type + [x, y], + [InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Decimal]); + + Assert.Null(firstResult); + + var result = await hybridWebView.InvokeJavaScriptAsync( + "EvaluateMeWithParamsAndVoidReturnGetResult", + InvokeJsonContext.Default.Decimal); + + Assert.Equal(777.777m, result); + }); + + [Theory] + [InlineData("")] + [InlineData("Async")] + public Task InvokeJavaScriptMethodThatThrowsNumber(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 1, ex => + { + Assert.Equal("InvokeJavaScript threw an exception: 777.777", ex.Message); + Assert.Equal("777.777", ex.InnerException?.Message); + Assert.Null(ex.InnerException?.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException?.StackTrace); + }); + + [Theory] + [InlineData("")] + [InlineData("Async")] + public Task InvokeJavaScriptMethodThatThrowsString(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 2, ex => + { + Assert.Equal("InvokeJavaScript threw an exception: String: 777.777", ex.Message); + Assert.Equal("String: 777.777", ex.InnerException?.Message); + Assert.Null(ex.InnerException?.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException?.StackTrace); + }); + + [Theory] + [InlineData("")] + [InlineData("Async")] + public Task InvokeJavaScriptMethodThatThrowsError(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 3, ex => + { + Assert.Equal("InvokeJavaScript threw an exception: Generic Error: 777.777", ex.Message); + Assert.Equal("Generic Error: 777.777", ex.InnerException?.Message); + Assert.Equal("Error", ex.InnerException?.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException?.StackTrace); + }); + + [Theory] + [InlineData("")] + [InlineData("Async")] + public Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 4, ex => + { + Assert.Contains("undefined", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("undefined", ex.InnerException?.Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("TypeError", ex.InnerException?.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException?.StackTrace); + }); + + Task RunExceptionTest(string method, int errorType, Action test) => + RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + var exception = await Assert.ThrowsAnyAsync(() => + hybridWebView.InvokeJavaScriptAsync( + method, + InvokeJsonContext.Default.Decimal, + [x, y, errorType], + [InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Decimal, InvokeJsonContext.Default.Int32])); + + test(exception); + }); + + public class ComputationResult + { + public decimal result { get; set; } + public string? operationName { get; set; } + } + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ComputationResult))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(decimal))] + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(int?))] + [JsonSerializable(typeof(double))] + [JsonSerializable(typeof(double?))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(Dictionary))] + internal partial class InvokeJsonContext : JsonSerializerContext + { + } +} diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SendRawMessage.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SendRawMessage.cs new file mode 100644 index 000000000000..39eef22edc96 --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SendRawMessage.cs @@ -0,0 +1,42 @@ +#nullable enable +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Maui.DeviceTests; + +[Category(TestCategory.HybridWebView)] +#if WINDOWS +[Collection(WebViewsCollection)] +#endif +public partial class HybridWebViewTests_SendRawMessage : HybridWebViewTestsBase +{ + [Fact] + public Task LoadsHtmlAndSendReceiveRawMessage() => + RunTest(async (hybridWebView) => + { + var lastRawMessage = ""; + + hybridWebView.RawMessageReceived += (s, e) => + { + lastRawMessage = e.Message; + }; + + const string TestRawMessage = "Hybrid\"\"'' {Test} with chars!"; + hybridWebView.SendRawMessage(TestRawMessage); + + var passed = false; + + for (var i = 0; i < 10; i++) + { + if (lastRawMessage == "You said: " + TestRawMessage) + { + passed = true; + break; + } + + await Task.Delay(1000); + } + + Assert.True(passed, $"Waited for raw message response but it never arrived or didn't match (last message: {lastRawMessage})"); + }); +} diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SetInvokeJavaScriptTarget.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SetInvokeJavaScriptTarget.cs new file mode 100644 index 000000000000..22603bfae6fb --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SetInvokeJavaScriptTarget.cs @@ -0,0 +1,176 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Maui.DeviceTests; + +[Category(TestCategory.HybridWebView)] +#if WINDOWS +[Collection(WebViewsCollection)] +#endif +public partial class HybridWebViewTests_SetInvokeJavaScriptTarget : HybridWebViewTestsBase +{ + [Theory] + [ClassData(typeof(InvokeJavaScriptAsyncTestData))] + public Task InvokeDotNet(string methodName, string expectedReturnValue) => + RunTest("invokedotnettests.html", async (hybridWebView) => + { + var invokeJavaScriptTarget = new TestDotNetMethods(); + hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget); + + //await Task.Delay(15_000); + + // Tell JavaScript to invoke the method + hybridWebView.SendRawMessage(methodName); + + // Wait for method invocation to complete + await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView); + + // Run some JavaScript to see if it got the expected result + var result = await hybridWebView.EvaluateJavaScriptAsync("GetLastScriptResult()"); + Assert.Equal(expectedReturnValue, result); + Assert.Equal(methodName, invokeJavaScriptTarget.LastMethodCalled); + }); + + private class TestDotNetMethods + { + private static ComputationResult NewComplexResult => + new ComputationResult { result = 123, operationName = "Test" }; + + public string? LastMethodCalled { get; private set; } + + public void Invoke_NoParam_NoReturn() + { + UpdateLastMethodCalled(); + } + + public object? Invoke_NoParam_ReturnNull() + { + UpdateLastMethodCalled(); + return null; + } + + public async Task Invoke_NoParam_ReturnTask() + { + await Task.Delay(1); + UpdateLastMethodCalled(); + } + + public async Task Invoke_NoParam_ReturnTaskNull() + { + await Task.Delay(1); + UpdateLastMethodCalled(); + return null; + } + + public async Task Invoke_NoParam_ReturnTaskValueType() + { + await Task.Delay(1); + UpdateLastMethodCalled(); + return 2; + } + + public async Task Invoke_NoParam_ReturnTaskComplex() + { + await Task.Delay(1); + UpdateLastMethodCalled(); + return NewComplexResult; + } + + public int Invoke_OneParam_ReturnValueType(Dictionary dict) + { + Assert.NotNull(dict); + Assert.Equal(2, dict.Count); + Assert.Equal(111, dict["first"]); + Assert.Equal(222, dict["second"]); + UpdateLastMethodCalled(); + return dict.Count; + } + + public Dictionary Invoke_OneParam_ReturnDictionary(Dictionary dict) + { + Assert.NotNull(dict); + Assert.Equal(2, dict.Count); + Assert.Equal(111, dict["first"]); + Assert.Equal(222, dict["second"]); + UpdateLastMethodCalled(); + dict["third"] = 333; + return dict; + } + + public ComputationResult Invoke_NullParam_ReturnComplex(object obj) + { + Assert.Null(obj); + UpdateLastMethodCalled(); + return NewComplexResult; + } + + public void Invoke_ManyParams_NoReturn(Dictionary dict, string str, object obj, ComputationResult computationResult, int[] arr) + { + Assert.NotNull(dict); + Assert.Equal(2, dict.Count); + Assert.Equal(111, dict["first"]); + Assert.Equal(222, dict["second"]); + + Assert.Equal("hello", str); + + Assert.Null(obj); + + Assert.NotNull(computationResult); + Assert.Equal("invoke_method", computationResult.operationName); + Assert.Equal(123.456m, computationResult.result, 6); + + Assert.NotNull(arr); + Assert.Equal(2, arr.Length); + Assert.Equal(111, arr[0]); + Assert.Equal(222, arr[1]); + + UpdateLastMethodCalled(); + } + + private void UpdateLastMethodCalled([CallerMemberName] string? methodName = null) + { + LastMethodCalled = methodName; + } + } + + private class InvokeJavaScriptAsyncTestData : IEnumerable + { + public IEnumerator GetEnumerator() + { + const string ComplexResult = "{\\\"result\\\":123,\\\"operationName\\\":\\\"Test\\\"}"; + const string DictionaryResult = "{\\\"first\\\":111,\\\"second\\\":222,\\\"third\\\":333}"; + const int ValueTypeResult = 2; + + // Test variations of: + // 1. Data type: ValueType, RefType, string, complex type + // 2. Containers of those types: array, dictionary + // 3. Methods with different return values (none, simple, complex, etc.) + yield return new object?[] { "Invoke_NoParam_NoReturn", null }; + yield return new object?[] { "Invoke_NoParam_ReturnNull", null }; + yield return new object?[] { "Invoke_NoParam_ReturnTask", null }; + yield return new object?[] { "Invoke_NoParam_ReturnTaskNull", null }; + yield return new object?[] { "Invoke_NoParam_ReturnTaskValueType", ValueTypeResult }; + yield return new object?[] { "Invoke_NoParam_ReturnTaskComplex", ComplexResult }; + yield return new object?[] { "Invoke_OneParam_ReturnValueType", ValueTypeResult }; + yield return new object?[] { "Invoke_OneParam_ReturnDictionary", DictionaryResult }; + yield return new object?[] { "Invoke_NullParam_ReturnComplex", ComplexResult }; + yield return new object?[] { "Invoke_ManyParams_NoReturn", null }; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + + public class ComputationResult + { + public decimal result { get; set; } + public string? operationName { get; set; } + } +} diff --git a/src/Controls/tests/DeviceTests/Elements/WebView/WebViewTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/WebView/WebViewTests.Windows.cs index 0206059c9d73..2b9e7f009777 100644 --- a/src/Controls/tests/DeviceTests/Elements/WebView/WebViewTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/WebView/WebViewTests.Windows.cs @@ -7,6 +7,9 @@ namespace Microsoft.Maui.DeviceTests { [Category(TestCategory.WebView)] +#if WINDOWS + [Collection(WebViewsCollection)] +#endif public partial class WebViewTests : ControlsHandlerTestBase { [Fact(DisplayName = "Evaluate JavaScript returning a String value" diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue30846.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue30846.cs new file mode 100644 index 000000000000..07936a38e65f --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue30846.cs @@ -0,0 +1,139 @@ +namespace TestCases.HostApp.Issues; + +[Issue(IssueTracker.Github, 30846, "Media Playback Customization in HybridWebView", PlatformAffected.All)] +public partial class Issue30846 : ContentPage +{ + Label _videoStatusLabel; + Label _audioStatusLabel; + + public Issue30846() + { + Title = "HybridWebView Autoplay Test"; + + var grid = new Grid + { + Padding = new Thickness(24), + RowSpacing = 8, + ColumnSpacing = 8, + RowDefinitions = + { + new RowDefinition(GridLength.Auto), // First row: switch and label + new RowDefinition(GridLength.Auto), // Second row: video status + new RowDefinition(GridLength.Auto), // Third row: audio status + new RowDefinition(GridLength.Star) // Forth row: webview container fills remaining height + }, + ColumnDefinitions = + { + new ColumnDefinition(GridLength.Auto), // First column: switch + new ColumnDefinition(GridLength.Star) // Second column: label + } + }; + + var webViewContainer = new ContentView + { + AutomationId = "WebViewContainer", + Content = CreateHybridWebView(true), + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Fill, + }; + grid.Add(webViewContainer); + Grid.SetRow(webViewContainer, 3); + Grid.SetColumnSpan(webViewContainer, 2); + + var switchControl = new Switch + { + AutomationId = "AutoPlaybackSwitch", + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Center, + IsToggled = false, + }; + switchControl.Toggled += (sender, e) => webViewContainer.Content = CreateHybridWebView(!e.Value); + Grid.SetRow(switchControl, 0); + Grid.SetColumn(switchControl, 0); + grid.Add(switchControl); + + var switchLabel = new Label + { + Text = "Auto Playback", + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Center, + }; + Grid.SetRow(switchLabel, 0); + Grid.SetColumn(switchLabel, 1); + grid.Add(switchLabel); + + _videoStatusLabel = new Label + { + AutomationId = "VideoStatusLabel", + Text = "Loading...", + }; + Grid.SetRow(_videoStatusLabel, 1); + Grid.SetColumnSpan(_videoStatusLabel, 2); + grid.Add(_videoStatusLabel); + + _audioStatusLabel = new Label + { + AutomationId = "AudioStatusLabel", + Text = "Loading...", + }; + Grid.SetRow(_audioStatusLabel, 2); + Grid.SetColumnSpan(_audioStatusLabel, 2); + grid.Add(_audioStatusLabel); + + Content = grid; + } + + HybridWebView CreateHybridWebView(bool requireUserGesture) + { + var hybridWebView = new HybridWebView + { + AutomationId = "HybridWebView", + DefaultFile = "issues/issue-30846.html", + HybridRoot = "hybridroot", + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Fill, + }; + + hybridWebView.RawMessageReceived += (s, e) => + { + Dispatcher.Dispatch(() => + { + if (e.Message.StartsWith("Video")) + _videoStatusLabel.Text = e.Message; + else if (e.Message.StartsWith("Audio")) + _audioStatusLabel.Text = e.Message; + }); + }; + + hybridWebView.WebViewInitializing += (s, e) => + { +#if IOS || MACCATALYST + // Make things look better + e.PlatformArgs.Configuration.AllowsInlineMediaPlayback = true; + + // Set media playback requirements based on the switch state + e.PlatformArgs.Configuration.MediaTypesRequiringUserActionForPlayback = requireUserGesture + ? WebKit.WKAudiovisualMediaTypes.All + : WebKit.WKAudiovisualMediaTypes.None; +#elif ANDROID + // Set media playback requirements based on the switch state + e.PlatformArgs.Settings.MediaPlaybackRequiresUserGesture = requireUserGesture; +#elif WINDOWS + // WebView2 requires that different environments have different UDF to support multiple simultaneous instances. + // Technically we are swapping out the instances, but the first instance takes time to shut down. + var lad = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + e.PlatformArgs.UserDataFolder = Path.Combine(lad, "Controls.TestCases.HostApp", $"UserDataFolder-{requireUserGesture}"); + + // Set media playback requirements based on the switch state + e.PlatformArgs.EnvironmentOptions = new Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions + { + AdditionalBrowserArguments = requireUserGesture + ? "" + : "--autoplay-policy=no-user-gesture-required" + }; +#endif + }; + + return hybridWebView; + } +} diff --git a/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/assets/horse.mp3 b/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/assets/horse.mp3 new file mode 100644 index 000000000000..5d1e6a91594b Binary files /dev/null and b/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/assets/horse.mp3 differ diff --git a/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/assets/mov_bbb.mp4 b/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/assets/mov_bbb.mp4 new file mode 100644 index 000000000000..0a4dd5b40171 Binary files /dev/null and b/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/assets/mov_bbb.mp4 differ diff --git a/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/issues/issue-30846.html b/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/issues/issue-30846.html new file mode 100644 index 000000000000..111e24134e39 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Resources/Raw/hybridroot/issues/issue-30846.html @@ -0,0 +1,71 @@ + + + + + WebView Autoplay Test + + + + +

WebView Autoplay Test

+

This page tries to autoplay a video and an audio element. If you see/hear them playing automatically, autoplay is enabled!

+ + +

+ + +

+ + + + \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30846.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30846.cs new file mode 100644 index 000000000000..d8d645c7fd77 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30846.cs @@ -0,0 +1,40 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue30846 : _IssuesUITest +{ + public Issue30846(TestDevice device) : base(device) + { + } + + public override string Issue => "Media Playback Customization in HybridWebView"; + + [Test] + [Category(UITestCategories.WebView)] + public void HybridWebView_Autoplay_Respects_UserGestureSetting() + { + // Wait for the page to load + App.WaitForElement("AutoPlaybackSwitch"); + + // Test with user gesture required (default: switch is off) + App.WaitForTextToBePresentInElement("VideoStatusLabel", "Video did not autoplay"); + App.WaitForTextToBePresentInElement("AudioStatusLabel", "Audio did not autoplay"); + + // Toggle the switch to allow autoplay (user gesture NOT required) + App.Tap("AutoPlaybackSwitch"); + + // Wait for the WebView to reload and update status + App.WaitForTextToBePresentInElement("VideoStatusLabel", "Video autoplayed!"); + App.WaitForTextToBePresentInElement("AudioStatusLabel", "Audio autoplayed!"); + + // Toggle the switch to allow autoplay (user gesture IS required) + App.Tap("AutoPlaybackSwitch"); + + // Test with user gesture required + App.WaitForTextToBePresentInElement("VideoStatusLabel", "Video did not autoplay"); + App.WaitForTextToBePresentInElement("AudioStatusLabel", "Audio did not autoplay"); + } +} diff --git a/src/Core/src/Core/IHybridWebView.cs b/src/Core/src/Core/IHybridWebView.cs index 7af0535c3147..553c13045c5b 100644 --- a/src/Core/src/Core/IHybridWebView.cs +++ b/src/Core/src/Core/IHybridWebView.cs @@ -5,7 +5,7 @@ namespace Microsoft.Maui { - public interface IHybridWebView : IView, IWebRequestInterceptingWebView + public interface IHybridWebView : IView, IWebRequestInterceptingWebView, IInitializationAwareWebView { /// /// Specifies the file within the that should be served as the default file. The diff --git a/src/Core/src/Core/IInitializationAwareWebView.cs b/src/Core/src/Core/IInitializationAwareWebView.cs new file mode 100644 index 000000000000..2bd5896e77c9 --- /dev/null +++ b/src/Core/src/Core/IInitializationAwareWebView.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Maui; + +public interface IInitializationAwareWebView : IView +{ + /// + /// Invoked when web view initialization is starting. This event allows the application to perform additional configuration. + /// +#if NETSTANDARD + void WebViewInitializationStarted(WebViewInitializationStartedEventArgs args); +#else + void WebViewInitializationStarted(WebViewInitializationStartedEventArgs args) { } +#endif + + /// + /// Invoked when the web view has been initialized. + /// +#if NETSTANDARD + void WebViewInitializationCompleted(WebViewInitializationCompletedEventArgs args); +#else + void WebViewInitializationCompleted(WebViewInitializationCompletedEventArgs args) { } +#endif +} diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Android.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Android.cs index efa88e623e50..30cac357aecd 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Android.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Android.cs @@ -35,6 +35,14 @@ protected override AWebView CreatePlatformView() _javaScriptInterface = new HybridWebViewJavaScriptInterface(this); platformView.AddJavascriptInterface(_javaScriptInterface, HybridWebViewHostJsName); + // Invoke the WebViewInitializing event to allow custom configuration of the web view + var initializingArgs = new WebViewInitializationStartedEventArgs(platformView.Settings); + VirtualView.WebViewInitializationStarted(initializingArgs); + + // Invoke the WebViewInitialized event to signal that the web view has been initialized + var initializedArgs = new WebViewInitializationCompletedEventArgs(platformView, platformView.Settings); + VirtualView?.WebViewInitializationCompleted(initializedArgs); + return platformView; } diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs index 4a911be52902..61e4e385d3d9 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs @@ -255,6 +255,7 @@ private sealed class HybridWebView2Proxy private Window? Window => _window is not null && _window.TryGetTarget(out var w) ? w : null; private HybridWebViewHandler? Handler => _handler is not null && _handler.TryGetTarget(out var h) ? h : null; + private IHybridWebView? VirtualView => Handler?.VirtualView; public void Connect(HybridWebViewHandler handler, WebView2 platformView) { @@ -265,11 +266,30 @@ public void Connect(HybridWebViewHandler handler, WebView2 platformView) private async Task TryInitializeWebView2(WebView2 webView) { - await webView.EnsureCoreWebView2Async(); + // Invoke the WebViewInitializing event to allow custom configuration of the web view + var initializingArgs = new WebViewInitializationStartedEventArgs(); + VirtualView?.WebViewInitializationStarted(initializingArgs); + + var env = await CoreWebView2Environment.CreateWithOptionsAsync( + browserExecutableFolder: initializingArgs.BrowserExecutableFolder, + userDataFolder: initializingArgs.UserDataFolder, + options: initializingArgs.EnvironmentOptions); + + var options = env.CreateCoreWebView2ControllerOptions(); + options.ScriptLocale = initializingArgs.ScriptLocale; + options.IsInPrivateModeEnabled = initializingArgs.IsInPrivateModeEnabled; + options.ProfileName = initializingArgs.ProfileName; + + await webView.EnsureCoreWebView2Async(env, options); webView.CoreWebView2.Settings.AreDevToolsEnabled = Handler?.DeveloperTools.Enabled ?? false; webView.CoreWebView2.Settings.IsWebMessageEnabled = true; - webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); + + webView.CoreWebView2.AddWebResourceRequestedFilter($"*", CoreWebView2WebResourceContext.All); + + // Invoke the WebViewInitialized event to signal that the web view has been initialized + var initializedArgs = new WebViewInitializationCompletedEventArgs(webView.CoreWebView2, webView.CoreWebView2.Settings); + VirtualView?.WebViewInitializationCompleted(initializedArgs); webView.WebMessageReceived += OnWebMessageReceived; webView.CoreWebView2.WebResourceRequested += OnWebResourceRequested; diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs index 6c7297e97d88..537475ec98d9 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs @@ -43,6 +43,10 @@ protected override WKWebView CreatePlatformView() // iOS WKWebView doesn't allow handling 'http'/'https' schemes, so we use the fake 'app' scheme config.SetUrlSchemeHandler(new SchemeHandler(this), urlScheme: "app"); + // Invoke the WebViewInitializing event to allow custom configuration of the web view + var initializingArgs = new WebViewInitializationStartedEventArgs(config); + VirtualView?.WebViewInitializationStarted(initializingArgs); + var webview = new MauiHybridWebView(this, RectangleF.Empty, config) { BackgroundColor = UIColor.Clear, @@ -61,6 +65,10 @@ protected override WKWebView CreatePlatformView() } } + // Invoke the WebViewInitialized event to signal that the web view has been initialized + var initializedArgs = new WebViewInitializationCompletedEventArgs(webview, config); + VirtualView?.WebViewInitializationCompleted(initializedArgs); + return webview; } diff --git a/src/Core/src/Primitives/WebViewInitializationCompletedEventArgs.cs b/src/Core/src/Primitives/WebViewInitializationCompletedEventArgs.cs new file mode 100644 index 000000000000..c6c2bf720065 --- /dev/null +++ b/src/Core/src/Primitives/WebViewInitializationCompletedEventArgs.cs @@ -0,0 +1,95 @@ +#if WINDOWS +using Microsoft.Web.WebView2.Core; +#elif IOS || MACCATALYST +using WebKit; +#elif ANDROID +using Android.Webkit; +#endif + +namespace Microsoft.Maui; + +/// +/// Provides platform-specific information for the event. +/// +public class WebViewInitializationCompletedEventArgs +{ +#if IOS || MACCATALYST + + /// + /// Initializes a new instance of the class. + /// + /// The native view that is being initialized. + /// The settings for the web view, which can be used to configure various aspects of the web view. + internal WebViewInitializationCompletedEventArgs(WKWebView sender, WKWebViewConfiguration configuration) + { + Sender = sender; + Configuration = configuration; + } + + /// + /// Gets the native view attached to the event. + /// + public WKWebView Sender { get; } + + /// + /// Gets or sets the settings attached to the web view. + /// + public WKWebViewConfiguration Configuration { get; } + +#elif ANDROID + + /// + /// Initializes a new instance of the class. + /// + /// The native view that is being initialized. + /// The settings for the web view, which can be used to configure various aspects of the web view. + internal WebViewInitializationCompletedEventArgs(WebView sender, WebSettings settings) + { + Sender = sender; + Settings = settings; + } + + /// + /// Gets the native view attached to the event. + /// + public WebView Sender { get; } + + /// + /// Gets or sets the settings attached to the web view. + /// + public WebSettings Settings { get; } + +#elif WINDOWS + + /// + /// Initializes a new instance of the class. + /// + /// The native view that is being initialized. + /// The settings for the web view, which can be used to configure various aspects of the web view. + internal WebViewInitializationCompletedEventArgs(CoreWebView2 sender, CoreWebView2Settings settings) + { + Sender = sender; + Settings = settings; + } + + /// + /// Gets the native view attached to the event. + /// + public CoreWebView2 Sender { get; } + + /// + /// Gets or sets the settings attached to the web view. + /// + public CoreWebView2Settings Settings { get; } + +#else + + /// + /// Initializes a new instance of the class. + /// + internal WebViewInitializationCompletedEventArgs() + { + } + +#endif +} diff --git a/src/Core/src/Primitives/WebViewInitializationStartedEventArgs.cs b/src/Core/src/Primitives/WebViewInitializationStartedEventArgs.cs new file mode 100644 index 000000000000..34316de7ca5e --- /dev/null +++ b/src/Core/src/Primitives/WebViewInitializationStartedEventArgs.cs @@ -0,0 +1,132 @@ +#if WINDOWS +using Microsoft.Web.WebView2.Core; +#elif IOS || MACCATALYST +using WebKit; +#elif ANDROID +using Android.Webkit; +#endif + +namespace Microsoft.Maui; + +/// +/// Provides platform-specific information for the event. +/// +public class WebViewInitializationStartedEventArgs +{ +#if WINDOWS + + /// + /// Initializes a new instance of the class. + /// + internal WebViewInitializationStartedEventArgs() + { + } + + /// + /// Gets or sets the relative path to the folder that contains a custom + /// version of WebView2 Runtime. + /// + /// + /// To use a fixed version of the WebView2 Runtime, set this property to + /// the folder path that contains the fixed version of the WebView2 Runtime. + /// + public string? BrowserExecutableFolder { get; set; } + + /// + /// Gets or sets the user data folder location for WebView2. + /// + /// + /// The default user data folder {Executable File Name}.WebView2 is created + /// in the same directory next to the compiled code for the app. + /// WebView2 creation fails if the compiled code is running in a directory + /// in which the process does not have permission to create a new directory. + /// The app is responsible to clean up the associated user data folder + /// when it is done. + /// + public string? UserDataFolder { get; set; } + + /// + /// Gets or sets the options used to create WebView2 Environment. + /// + /// + /// As a browser process may be shared among WebViews, WebView creation fails + /// if the specified options does not match the options of the WebViews + /// that are currently running in the shared browser process. + /// + public CoreWebView2EnvironmentOptions? EnvironmentOptions { get; set; } + + /// + /// Gets or sets whether the WebView2 controller is in private mode. + /// + public bool IsInPrivateModeEnabled { get; set; } + + /// + /// Gets or sets the name of the controller profile. + /// + /// + /// Profile names are only allowed to contain the following ASCII characters: + /// * alphabet characters: a-z and A-Z + /// * digit characters: 0-9 + /// * and '#', '@', '$', '(', ')', '+', '-', '_', '~', '.', ' ' (space). + /// It has a maximum length of 64 characters excluding the null-terminator. + /// It is ASCII case insensitive. + /// + public string? ProfileName { get; set; } + + /// + /// Gets or sets the controller's default script locale. + /// + /// + /// This property sets the default locale for all Intl JavaScript APIs and other JavaScript APIs that + /// depend on it, namely Intl.DateTimeFormat() which affects string formatting like in the time/date + /// formats. The intended locale value is in the format of BCP 47 Language Tags. + /// More information can be found from https://www.ietf.org/rfc/bcp/bcp47.html. + /// The default value for ScriptLocale will be depend on the WebView2 language and OS region. + /// If the language portions of the WebView2 language and OS region match, then it will use the OS region. + /// Otherwise, it will use the WebView2 language. + /// + public string? ScriptLocale { get; set; } + +#elif IOS || MACCATALYST + + /// + /// Initializes a new instance of the class. + /// + /// The configuration to be used in the construction of the WKWebView instance. + internal WebViewInitializationStartedEventArgs(WKWebViewConfiguration configuration) + { + Configuration = configuration; + } + + /// + /// Gets or sets the configuration to be used in the construction of the WKWebView instance. + /// + public WKWebViewConfiguration Configuration { get; } + +#elif ANDROID + + /// + /// Initializes a new instance of the class. + /// + /// The settings for the WebView. + internal WebViewInitializationStartedEventArgs(WebSettings settings) + { + Settings = settings; + } + + /// + /// Gets the platform-specific settings for the WebView. + /// + public WebSettings Settings { get; } + +#else + + /// + /// Initializes a new instance of the class. + /// + internal WebViewInitializationStartedEventArgs() + { + } + +#endif +} diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 38392a8ddffa..4d22a7dae776 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -39,6 +39,9 @@ Microsoft.Maui.IDatePicker.IsOpen.set -> void Microsoft.Maui.IDatePicker.MaximumDate.get -> System.DateTime? *REMOVED*Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime? +Microsoft.Maui.IInitializationAwareWebView +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationCompleted(Microsoft.Maui.WebViewInitializationCompletedEventArgs! args) -> void +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationStarted(Microsoft.Maui.WebViewInitializationStartedEventArgs! args) -> void Microsoft.Maui.IPicker.IsOpen.get -> bool Microsoft.Maui.IPicker.IsOpen.set -> void Microsoft.Maui.ISearchBar.ReturnType.get -> Microsoft.Maui.ReturnType @@ -94,6 +97,11 @@ Microsoft.Maui.WebResourceRequestedEventArgs.Request.get -> Android.Webkit.IWebR Microsoft.Maui.WebResourceRequestedEventArgs.Response.get -> Android.Webkit.WebResourceResponse? Microsoft.Maui.WebResourceRequestedEventArgs.Response.set -> void Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> Android.Webkit.WebView! +Microsoft.Maui.WebViewInitializationCompletedEventArgs +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Sender.get -> Android.Webkit.WebView! +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Settings.get -> Android.Webkit.WebSettings! +Microsoft.Maui.WebViewInitializationStartedEventArgs +Microsoft.Maui.WebViewInitializationStartedEventArgs.Settings.get -> Android.Webkit.WebSettings! override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 910fbccdb730..c218286ceb59 100644 --- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -36,6 +36,9 @@ Microsoft.Maui.IDatePicker.IsOpen.set -> void Microsoft.Maui.IDatePicker.MaximumDate.get -> System.DateTime? *REMOVED*Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime? +Microsoft.Maui.IInitializationAwareWebView +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationCompleted(Microsoft.Maui.WebViewInitializationCompletedEventArgs! args) -> void +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationStarted(Microsoft.Maui.WebViewInitializationStartedEventArgs! args) -> void Microsoft.Maui.IPicker.IsOpen.get -> bool Microsoft.Maui.IPicker.IsOpen.set -> void Microsoft.Maui.ISearchBar.ReturnType.get -> Microsoft.Maui.ReturnType @@ -87,6 +90,11 @@ Microsoft.Maui.SwipeViewSwipeStarted.SwipeViewSwipeStarted(Microsoft.Maui.SwipeV Microsoft.Maui.WebResourceRequestedEventArgs Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! Microsoft.Maui.WebResourceRequestedEventArgs.UrlSchemeTask.get -> WebKit.IWKUrlSchemeTask! +Microsoft.Maui.WebViewInitializationCompletedEventArgs +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.WebViewInitializationStartedEventArgs +Microsoft.Maui.WebViewInitializationStartedEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 48c31ece3f70..285d9e1e54a7 100644 --- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -36,6 +36,9 @@ Microsoft.Maui.IDatePicker.IsOpen.set -> void Microsoft.Maui.IDatePicker.MaximumDate.get -> System.DateTime? *REMOVED*Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime? +Microsoft.Maui.IInitializationAwareWebView +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationCompleted(Microsoft.Maui.WebViewInitializationCompletedEventArgs! args) -> void +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationStarted(Microsoft.Maui.WebViewInitializationStartedEventArgs! args) -> void Microsoft.Maui.IPicker.IsOpen.get -> bool Microsoft.Maui.IPicker.IsOpen.set -> void Microsoft.Maui.ISearchBar.ReturnType.get -> Microsoft.Maui.ReturnType @@ -89,6 +92,11 @@ Microsoft.Maui.SwipeViewSwipeStarted.SwipeViewSwipeStarted(Microsoft.Maui.SwipeV Microsoft.Maui.WebResourceRequestedEventArgs Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> WebKit.WKWebView! Microsoft.Maui.WebResourceRequestedEventArgs.UrlSchemeTask.get -> WebKit.IWKUrlSchemeTask! +Microsoft.Maui.WebViewInitializationCompletedEventArgs +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Sender.get -> WebKit.WKWebView! +Microsoft.Maui.WebViewInitializationStartedEventArgs +Microsoft.Maui.WebViewInitializationStartedEventArgs.Configuration.get -> WebKit.WKWebViewConfiguration! override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 555f9aa99020..c79a8409bb80 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -36,6 +36,9 @@ Microsoft.Maui.IDatePicker.IsOpen.set -> void Microsoft.Maui.IDatePicker.MaximumDate.get -> System.DateTime? *REMOVED*Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime? +Microsoft.Maui.IInitializationAwareWebView +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationCompleted(Microsoft.Maui.WebViewInitializationCompletedEventArgs! args) -> void +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationStarted(Microsoft.Maui.WebViewInitializationStartedEventArgs! args) -> void Microsoft.Maui.IPicker.IsOpen.get -> bool Microsoft.Maui.IPicker.IsOpen.set -> void Microsoft.Maui.ISearchBar.ReturnType.get -> Microsoft.Maui.ReturnType @@ -80,6 +83,22 @@ Microsoft.Maui.SwipeViewSwipeStarted.SwipeViewSwipeStarted(Microsoft.Maui.SwipeV Microsoft.Maui.WebResourceRequestedEventArgs Microsoft.Maui.WebResourceRequestedEventArgs.RequestEventArgs.get -> Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs! Microsoft.Maui.WebResourceRequestedEventArgs.Sender.get -> Microsoft.Web.WebView2.Core.CoreWebView2! +Microsoft.Maui.WebViewInitializationCompletedEventArgs +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Sender.get -> Microsoft.Web.WebView2.Core.CoreWebView2! +Microsoft.Maui.WebViewInitializationCompletedEventArgs.Settings.get -> Microsoft.Web.WebView2.Core.CoreWebView2Settings! +Microsoft.Maui.WebViewInitializationStartedEventArgs +Microsoft.Maui.WebViewInitializationStartedEventArgs.BrowserExecutableFolder.get -> string? +Microsoft.Maui.WebViewInitializationStartedEventArgs.BrowserExecutableFolder.set -> void +Microsoft.Maui.WebViewInitializationStartedEventArgs.EnvironmentOptions.get -> Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions? +Microsoft.Maui.WebViewInitializationStartedEventArgs.EnvironmentOptions.set -> void +Microsoft.Maui.WebViewInitializationStartedEventArgs.IsInPrivateModeEnabled.get -> bool +Microsoft.Maui.WebViewInitializationStartedEventArgs.IsInPrivateModeEnabled.set -> void +Microsoft.Maui.WebViewInitializationStartedEventArgs.ProfileName.get -> string? +Microsoft.Maui.WebViewInitializationStartedEventArgs.ProfileName.set -> void +Microsoft.Maui.WebViewInitializationStartedEventArgs.ScriptLocale.get -> string? +Microsoft.Maui.WebViewInitializationStartedEventArgs.ScriptLocale.set -> void +Microsoft.Maui.WebViewInitializationStartedEventArgs.UserDataFolder.get -> string? +Microsoft.Maui.WebViewInitializationStartedEventArgs.UserDataFolder.set -> void override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? diff --git a/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt index 8354318d2602..31a42955414e 100644 --- a/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -36,6 +36,9 @@ Microsoft.Maui.IDatePicker.IsOpen.set -> void Microsoft.Maui.IDatePicker.MaximumDate.get -> System.DateTime? *REMOVED*Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime? +Microsoft.Maui.IInitializationAwareWebView +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationCompleted(Microsoft.Maui.WebViewInitializationCompletedEventArgs! args) -> void +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationStarted(Microsoft.Maui.WebViewInitializationStartedEventArgs! args) -> void Microsoft.Maui.IPicker.IsOpen.get -> bool Microsoft.Maui.IPicker.IsOpen.set -> void Microsoft.Maui.ISearchBar.ReturnType.get -> Microsoft.Maui.ReturnType @@ -75,6 +78,8 @@ Microsoft.Maui.SwipeViewSwipeEnded.SwipeViewSwipeEnded(Microsoft.Maui.SwipeViewS Microsoft.Maui.SwipeViewSwipeStarted.Deconstruct(out Microsoft.Maui.SwipeDirection SwipeDirection) -> void Microsoft.Maui.SwipeViewSwipeStarted.SwipeViewSwipeStarted(Microsoft.Maui.SwipeViewSwipeStarted! original) -> void Microsoft.Maui.WebResourceRequestedEventArgs +Microsoft.Maui.WebViewInitializationCompletedEventArgs +Microsoft.Maui.WebViewInitializationStartedEventArgs override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? diff --git a/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 8354318d2602..31a42955414e 100644 --- a/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -36,6 +36,9 @@ Microsoft.Maui.IDatePicker.IsOpen.set -> void Microsoft.Maui.IDatePicker.MaximumDate.get -> System.DateTime? *REMOVED*Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime? +Microsoft.Maui.IInitializationAwareWebView +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationCompleted(Microsoft.Maui.WebViewInitializationCompletedEventArgs! args) -> void +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationStarted(Microsoft.Maui.WebViewInitializationStartedEventArgs! args) -> void Microsoft.Maui.IPicker.IsOpen.get -> bool Microsoft.Maui.IPicker.IsOpen.set -> void Microsoft.Maui.ISearchBar.ReturnType.get -> Microsoft.Maui.ReturnType @@ -75,6 +78,8 @@ Microsoft.Maui.SwipeViewSwipeEnded.SwipeViewSwipeEnded(Microsoft.Maui.SwipeViewS Microsoft.Maui.SwipeViewSwipeStarted.Deconstruct(out Microsoft.Maui.SwipeDirection SwipeDirection) -> void Microsoft.Maui.SwipeViewSwipeStarted.SwipeViewSwipeStarted(Microsoft.Maui.SwipeViewSwipeStarted! original) -> void Microsoft.Maui.WebResourceRequestedEventArgs +Microsoft.Maui.WebViewInitializationCompletedEventArgs +Microsoft.Maui.WebViewInitializationStartedEventArgs override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? diff --git a/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 8354318d2602..31a42955414e 100644 --- a/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -36,6 +36,9 @@ Microsoft.Maui.IDatePicker.IsOpen.set -> void Microsoft.Maui.IDatePicker.MaximumDate.get -> System.DateTime? *REMOVED*Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime Microsoft.Maui.IDatePicker.MinimumDate.get -> System.DateTime? +Microsoft.Maui.IInitializationAwareWebView +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationCompleted(Microsoft.Maui.WebViewInitializationCompletedEventArgs! args) -> void +Microsoft.Maui.IInitializationAwareWebView.WebViewInitializationStarted(Microsoft.Maui.WebViewInitializationStartedEventArgs! args) -> void Microsoft.Maui.IPicker.IsOpen.get -> bool Microsoft.Maui.IPicker.IsOpen.set -> void Microsoft.Maui.ISearchBar.ReturnType.get -> Microsoft.Maui.ReturnType @@ -75,6 +78,8 @@ Microsoft.Maui.SwipeViewSwipeEnded.SwipeViewSwipeEnded(Microsoft.Maui.SwipeViewS Microsoft.Maui.SwipeViewSwipeStarted.Deconstruct(out Microsoft.Maui.SwipeDirection SwipeDirection) -> void Microsoft.Maui.SwipeViewSwipeStarted.SwipeViewSwipeStarted(Microsoft.Maui.SwipeViewSwipeStarted! original) -> void Microsoft.Maui.WebResourceRequestedEventArgs +Microsoft.Maui.WebViewInitializationCompletedEventArgs +Microsoft.Maui.WebViewInitializationStartedEventArgs override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool override Microsoft.Maui.Converters.EasingTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?