diff --git a/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js b/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js index 706e3ffaaa3b..52d67172af49 100644 --- a/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js +++ b/src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/scripts/HybridWebView.js @@ -91,20 +91,49 @@ } }, - "__InvokeJavaScript": function __InvokeJavaScript(taskId, methodName, args) { - if (methodName[Symbol.toStringTag] === 'AsyncFunction') { - // For async methods, we need to call the method and then trigger the callback when it's done - const asyncPromise = methodName(...args); - asyncPromise - .then(asyncResult => { - window.HybridWebView.__TriggerAsyncCallback(taskId, asyncResult); - }) - .catch(error => console.error(error)); + "__InvokeJavaScript": async function __InvokeJavaScript(taskId, methodName, args) { + try { + var result = null; + if (methodName[Symbol.toStringTag] === 'AsyncFunction') { + result = await methodName(...args); + } else { + result = methodName(...args); + } + window.HybridWebView.__TriggerAsyncCallback(taskId, result); + } catch (ex) { + console.error(ex); + window.HybridWebView.__TriggerAsyncFailedCallback(taskId, ex); + } + }, + + "__TriggerAsyncFailedCallback": function __TriggerAsyncCallback(taskId, error) { + + if (!error) { + json = { + Message: "Unknown error", + StackTrace: Error().stack + }; + } else if (error instanceof Error) { + json = { + Name: error.name, + Message: error.message, + StackTrace: error.stack + }; + } else if (typeof (error) === 'string') { + json = { + Message: error, + StackTrace: Error().stack + }; } else { - // For sync methods, we can call the method and trigger the callback immediately - const syncResult = methodName(...args); - window.HybridWebView.__TriggerAsyncCallback(taskId, syncResult); + json = { + Message: JSON.stringify(error), + StackTrace: Error().stack + }; } + + json = JSON.stringify(json); + + window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptFailed', taskId + '|' + json); }, "__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) { diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index 500e24eeaf70..6007d702e75f 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -189,6 +189,12 @@ static MauiAppBuilder SetupDefaults(this MauiAppBuilder builder) handlers.AddControlsHandlers(); }); + // NOTE: not registered under NativeAOT or TrimMode=Full scenarios + if (RuntimeFeature.IsHybridWebViewSupported) + { + builder.Services.AddScoped(_ => new HybridWebViewTaskManager()); + } + #if WINDOWS builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); #endif diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs index c78e24bb5a44..1637ec4b0063 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; @@ -24,6 +25,7 @@ void SetupBuilder() }); builder.Services.AddHybridWebViewDeveloperTools(); + builder.Services.AddScoped(); }); } @@ -311,6 +313,106 @@ public Task InvokeDotNet(string methodName, string expectedReturnValue) => Assert.Equal(methodName, invokeJavaScriptTarget.LastMethodCalled); }); + [Theory] + [InlineData("")] + [InlineData("Async")] + public async Task InvokeJavaScriptMethodThatThrowsNumber(string type) + { +#if ANDROID + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + { + return; + } +#endif + + var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 1); + 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 async Task InvokeJavaScriptMethodThatThrowsString(string type) + { +#if ANDROID + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + { + return; + } +#endif + + var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 2); + 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 async Task InvokeJavaScriptMethodThatThrowsError(string type) + { +#if ANDROID + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + { + return; + } +#endif + + var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 3); + 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 async Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) + { +#if ANDROID + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + { + return; + } +#endif + + var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 4); + 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); + } + + async Task RunExceptionTest(string method, int errorType) + { + Exception exception = null; + + await RunTest(async (hybridWebView) => + { + var x = 123.456m; + var y = 654.321m; + + exception = await Assert.ThrowsAnyAsync(() => + hybridWebView.InvokeJavaScriptAsync( + method, + HybridWebViewTestContext.Default.Decimal, + [x, y, errorType], + [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Int32])); + }); + + return exception; + } + Task RunTest(Func test) => RunTest(null, test); @@ -460,6 +562,7 @@ public class ComputationResult [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(ComputationResult))] + [JsonSerializable(typeof(int))] [JsonSerializable(typeof(decimal))] [JsonSerializable(typeof(bool))] [JsonSerializable(typeof(int))] diff --git a/src/Controls/tests/DeviceTests/MauiProgram.cs b/src/Controls/tests/DeviceTests/MauiProgram.cs index c9358470187d..61c75be8df00 100644 --- a/src/Controls/tests/DeviceTests/MauiProgram.cs +++ b/src/Controls/tests/DeviceTests/MauiProgram.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Reflection; using Microsoft.Maui.Controls; using Microsoft.Maui.Hosting; @@ -9,6 +10,11 @@ namespace Microsoft.Maui.DeviceTests { public static class MauiProgram { + static MauiProgram() + { + AppContext.SetSwitch("HybridWebView.InvokeJavaScriptThrowsExceptions", isEnabled: true); + } + #if ANDROID public static Android.Content.Context CurrentContext => MauiProgramDefaults.DefaultContext; #elif WINDOWS diff --git a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html index 82a1c5a70e41..63c20576e871 100644 --- a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html +++ b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html @@ -6,7 +6,19 @@ + + + + + + +