Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
Text="Invoke Async JS"
Clicked="InvokeAsyncJSMethodButton_Clicked" />

<Button
Margin="10"
Text="Test JS Exception"
Clicked="InvokeJSExceptionButton_Clicked" />

<Button
Margin="10"
Text="Test JS Async Exception"
Clicked="InvokeJSAsyncExceptionButton_Clicked" />

</VerticalStackLayout>

<HybridWebView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,52 @@ private void hwv_RawMessageReceived(object sender, HybridWebViewRawMessageReceiv
Dispatcher.Dispatch(() => statusText.Text += Environment.NewLine + e.Message);
}

private async void InvokeJSExceptionButton_Clicked(object sender, EventArgs e)
{
var statusResult = "";

try
{
statusResult += Environment.NewLine + "Calling JavaScript function that throws exception...";
var result = await hwv.InvokeJavaScriptAsync<string>(
"ThrowJavaScriptError",
SampleInvokeJsContext.Default.String,
[],
[]);

statusResult += Environment.NewLine + "JavaScript function unexpectedly succeeded with result: " + result;
}
catch (Exception ex)
{
statusResult += Environment.NewLine + $"Caught JavaScript exception in C#: {ex.GetType().Name} - {ex.Message}";
}

Dispatcher.Dispatch(() => statusText.Text += statusResult);
}

private async void InvokeJSAsyncExceptionButton_Clicked(object sender, EventArgs e)
{
var statusResult = "";

try
{
statusResult += Environment.NewLine + "Calling async JavaScript function that throws exception...";
var result = await hwv.InvokeJavaScriptAsync<string>(
"ThrowJavaScriptErrorAsync",
SampleInvokeJsContext.Default.String,
[],
[]);

statusResult += Environment.NewLine + "Async JavaScript function unexpectedly succeeded with result: " + result;
}
catch (Exception ex)
{
statusResult += Environment.NewLine + $"Caught async JavaScript exception in C#: {ex.GetType().Name} - {ex.Message}";
}

Dispatcher.Dispatch(() => statusText.Text += statusResult);
}

public class ComputationResult
{
public double result { get; set; }
Expand Down Expand Up @@ -153,6 +199,21 @@ public async Task<SyncReturn> DoAsyncWorkParamsReturn(int i, string s)
Value = i,
};
}

// Demo method that throws an exception to showcase error handling
public void ThrowException()
{
Debug.WriteLine("ThrowException called - about to throw");
throw new InvalidOperationException("This is a test exception thrown from C# code!");
}

// Demo async method that throws an exception
public async Task<string> ThrowExceptionAsync()
{
Debug.WriteLine("ThrowExceptionAsync called - about to throw");
await Task.Delay(100);
throw new ArgumentException("This is an async test exception thrown from C# code!");
}
}

public class SyncReturn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@
LogMessage("Invoked DoAsyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value);
}

// Demo functions to test exception handling
async function InvokeThrowException() {
LogMessage("Invoking ThrowException");
try {
await window.HybridWebView.InvokeDotNet('ThrowException');
LogMessage("ThrowException unexpectedly succeeded!");
} catch (ex) {
LogMessage("Caught exception from ThrowException: " + ex.message);
LogMessage("Exception type: " + (ex.dotNetErrorType || "Unknown"));
if (ex.dotNetStackTrace) {
LogMessage("Stack trace: " + ex.dotNetStackTrace.substring(0, 200) + "...");
}
}
}

async function InvokeThrowExceptionAsync() {
LogMessage("Invoking ThrowExceptionAsync");
try {
const result = await window.HybridWebView.InvokeDotNet('ThrowExceptionAsync');
LogMessage("ThrowExceptionAsync unexpectedly succeeded with result: " + result);
} catch (ex) {
LogMessage("Caught async exception from ThrowExceptionAsync: " + ex.message);
LogMessage("Exception type: " + (ex.dotNetErrorType || "Unknown"));
}
}

// JavaScript functions that throw exceptions (for testing C# exception handling of JS calls)
function ThrowJavaScriptError() {
LogMessage("ThrowJavaScriptError called - about to throw");
throw new Error("This is a test error thrown from JavaScript code!");
}

async function ThrowJavaScriptErrorAsync() {
LogMessage("ThrowJavaScriptErrorAsync called - about to throw");
await new Promise(resolve => setTimeout(resolve, 100));
throw new Error("This is an async test error thrown from JavaScript code!");
}

</script>
</head>
<body>
Expand All @@ -114,6 +152,16 @@
<button onclick="InvokeDoAsyncWorkReturn()">Call C# async method (no params) and get simple return value</button>
<button onclick="InvokeDoAsyncWorkParamsReturn()">Call C# async method (params) and get complex return value</button>
</div>
<div>
<strong>Exception Handling Tests:</strong>
<button onclick="InvokeThrowException()">Test C# Exception Handling</button>
<button onclick="InvokeThrowExceptionAsync()">Test C# Async Exception Handling</button>
</div>
<div>
<strong>JavaScript Exception Tests (called from C#):</strong>
<button onclick="ThrowJavaScriptError()">Test JS Exception</button>
<button onclick="ThrowJavaScriptErrorAsync()">Test JS Async Exception</button>
</div>
<div>
Log: <textarea readonly id="messageLog" style="width: 80%; height: 10em;"></textarea>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Maui.DeviceTests;

[Category(TestCategory.HybridWebView)]
#if WINDOWS
[Collection(WebViewsCollection)]
#endif
public partial class HybridWebViewTests_ExceptionHandling : HybridWebViewTestsBase
{
[Fact]
public Task CSharpMethodThatThrowsException_ShouldPropagateToJavaScript() =>
RunTest("exception-tests.html", async (hybridWebView) =>
{
var invokeJavaScriptTarget = new TestExceptionMethods();
hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget);

// Tell JavaScript to invoke the method that throws an exception
hybridWebView.SendRawMessage("ThrowException");

// Wait for JavaScript to handle the exception
await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView);

// Check that JavaScript caught the exception
var caughtError = await hybridWebView.EvaluateJavaScriptAsync("GetLastError()");
Assert.Equal("Test exception message", caughtError);

// Check that the method was called before throwing
Assert.Equal("ThrowException", invokeJavaScriptTarget.LastMethodCalled);
});

[Fact]
public Task CSharpAsyncMethodThatThrowsException_ShouldPropagateToJavaScript() =>
RunTest("exception-tests.html", async (hybridWebView) =>
{
var invokeJavaScriptTarget = new TestExceptionMethods();
hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget);

// Tell JavaScript to invoke the async method that throws an exception
hybridWebView.SendRawMessage("ThrowExceptionAsync");

// Wait for JavaScript to handle the exception
await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView);

// Check that JavaScript caught the async exception
var caughtError = await hybridWebView.EvaluateJavaScriptAsync("GetLastError()");
Assert.Equal("Async test exception", caughtError);

// Check that the method was called before throwing
Assert.Equal("ThrowExceptionAsync", invokeJavaScriptTarget.LastMethodCalled);
});

[Fact]
public Task CSharpMethodThatThrowsCustomException_ShouldIncludeExceptionDetails() =>
RunTest("exception-tests.html", async (hybridWebView) =>
{
var invokeJavaScriptTarget = new TestExceptionMethods();
hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget);

// Tell JavaScript to invoke the method that throws a custom exception
hybridWebView.SendRawMessage("ThrowCustomException");

// Wait for JavaScript to handle the exception
await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView);

// Check that JavaScript caught the exception with custom details
var errorType = await hybridWebView.EvaluateJavaScriptAsync("GetLastErrorType()");
var errorMessage = await hybridWebView.EvaluateJavaScriptAsync("GetLastErrorMessage()");

Assert.Equal("ArgumentException", errorType);
Assert.Equal("Custom argument exception", errorMessage);
});

[Fact]
public Task CSharpMethodThatSucceeds_ShouldStillWorkNormally() =>
RunTest("exception-tests.html", async (hybridWebView) =>
{
var invokeJavaScriptTarget = new TestExceptionMethods();
hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget);

// Tell JavaScript to invoke a normal method that doesn't throw
hybridWebView.SendRawMessage("SuccessMethod");

// Wait for JavaScript to complete
await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView);

// Check that JavaScript got the normal result
var result = await hybridWebView.EvaluateJavaScriptAsync("GetLastResult()");
Assert.Contains("Success!", result, StringComparison.Ordinal);

// Check that no error was thrown
var hasError = await hybridWebView.EvaluateJavaScriptAsync("HasError()");
Assert.Contains("false", hasError, StringComparison.Ordinal);
});

private class TestExceptionMethods
{
public string? LastMethodCalled { get; private set; }

public void ThrowException()
{
LastMethodCalled = nameof(ThrowException);
throw new InvalidOperationException("Test exception message");
}

public void ThrowCustomException()
{
LastMethodCalled = nameof(ThrowCustomException);
throw new ArgumentException("Custom argument exception");
}

public async Task ThrowExceptionAsync()
{
LastMethodCalled = nameof(ThrowExceptionAsync);
await Task.Delay(10); // Make it actually async
throw new InvalidOperationException("Async test exception");
}

public string SuccessMethod()
{
LastMethodCalled = nameof(SuccessMethod);
return "Success!";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<link rel="icon" href="data:,">
<script src="_framework/hybridwebview.js"></script>
<script>
window.addEventListener(
"HybridWebViewMessageReceived",
async function (e) {
var methodToInvoke = e.detail.message;
var result = null;
var error = null;
var hasError = false;

try {
// Invoke the requested method
result = await window.HybridWebView.InvokeDotNet(methodToInvoke);
} catch (ex) {
hasError = true;
error = {
message: ex.message,
type: ex.dotNetErrorType || ex.name,
stackTrace: ex.dotNetStackTrace || ex.stack
};
console.error('Caught exception from .NET method:', ex);
}

// Store the results so they can be checked in the test
lastResult = result;
lastError = error;
lastHasError = hasError;
SetStatusDone();
});

var lastResult = null;
var lastError = null;
var lastHasError = false;

function GetLastResult() {
return lastResult === null ? null : JSON.stringify(lastResult);
}

function GetLastError() {
return lastError ? lastError.message : null;
}

function GetLastErrorType() {
return lastError ? lastError.type : null;
}

function GetLastErrorMessage() {
return lastError ? lastError.message : null;
}

function HasError() {
return lastHasError.toString();
}

function SetStatusDone() {
document.getElementById('status').innerHTML = "Done!";
}

</script>
</head>
<body>
<div>
Hybrid exception test!
</div>
<div id="htmlLoaded"></div>
<div id="status"></div>
</body>
</html>
11 changes: 11 additions & 0 deletions src/Core/src/Handlers/HybridWebView/HybridWebView.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,17 @@
if (!response) {
return null;
}
// Check if the response indicates an error
if (response.IsError) {
const error = new Error(response.ErrorMessage || 'Unknown error occurred in .NET method');
if (response.ErrorType) {
error.dotNetErrorType = response.ErrorType;
}
if (response.ErrorStackTrace) {
error.dotNetStackTrace = response.ErrorStackTrace;
}
throw error;
}
if (response.IsJson) {
return JSON.parse(response.Result);
}
Expand Down
Loading
Loading