Skip to content
Closed
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 @@ -391,6 +391,76 @@ public Task InvokeDotNet(string methodName, string expectedReturnValue) =>
Assert.Equal(methodName, invokeJavaScriptTarget.LastMethodCalled);
});

[Theory]
[ClassData(typeof(InvokeDotNetExceptionTestData))]
public Task InvokeDotNetExceptionHandling(string methodName, string expectedExceptionType, string expectedExceptionMessage) =>
RunTest("invokedotnetexceptiontests.html", async (hybridWebView) =>
{
var invokeJavaScriptTarget = new TestDotNetMethods();
hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget);

// Tell JavaScript to invoke the method that throws
hybridWebView.SendRawMessage(methodName + (methodName.Contains("ArgumentException", StringComparison.Ordinal) ? "|test_param" : ""));

// Wait for method invocation to complete (should result in error)
await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView);

// Run some JavaScript to check if the exception was caught and handled properly
var errorInfo = await hybridWebView.EvaluateJavaScriptAsync("GetLastErrorInfo()");

// Parse the error information
Assert.NotNull(errorInfo);
Assert.Contains(expectedExceptionType, errorInfo, StringComparison.Ordinal);
Assert.Contains(expectedExceptionMessage, errorInfo, StringComparison.Ordinal);

// Verify the method was called (to ensure we reached the C# method before it threw)
Assert.Equal(methodName, invokeJavaScriptTarget.LastMethodCalled);
});

[Fact]
public Task DemonstrateExceptionHandlingFunctionality() =>
RunTest("exceptiondemo.html", async (hybridWebView) =>
{
var invokeJavaScriptTarget = new TestDotNetMethods();
hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget);

// Test successful method call
var successResult = await hybridWebView.EvaluateJavaScriptAsync("testSuccessfulCall()");

// Wait a bit for the operation to complete
await Task.Delay(1000);

// Test exception handling - this should NOT throw an exception in C# test code
// because the JavaScript should catch it
var exceptionResult = await hybridWebView.EvaluateJavaScriptAsync("testExceptionCall()");

// Wait a bit for the operation to complete
await Task.Delay(1000);

// Verify both methods were called
// Note: ThrowTestException should be the last method called since it executes after GetSuccessMessage
Assert.Equal("ThrowTestException", invokeJavaScriptTarget.LastMethodCalled);
});

[Fact]
public Task InvokeDotNetExceptionPreservesStackTrace() =>
RunTest("invokedotnetexceptiontests.html", async (hybridWebView) =>
{
var invokeJavaScriptTarget = new TestDotNetMethods();
hybridWebView.SetInvokeJavaScriptTarget(invokeJavaScriptTarget);

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

// Wait for method invocation to complete (should result in error)
await WebViewHelpers.WaitForHtmlStatusSet(hybridWebView);

// Verify that the .NET stack trace is preserved
var stackTrace = await hybridWebView.EvaluateJavaScriptAsync("GetLastErrorStackTrace()");
Assert.NotNull(stackTrace);
Assert.Contains("Invoke_ThrowsInvalidOperationException", stackTrace, StringComparison.Ordinal);
});

[Theory]
[InlineData("")]
[InlineData("Async")]
Expand Down Expand Up @@ -621,12 +691,68 @@ public void Invoke_ManyParams_NoReturn(Dictionary<string, int> dict, string str,
UpdateLastMethodCalled();
}

// New test methods for exception handling
public void Invoke_ThrowsInvalidOperationException()
{
UpdateLastMethodCalled();
throw new InvalidOperationException("Test invalid operation exception message");
}

public void Invoke_ThrowsArgumentException(string message)
{
UpdateLastMethodCalled();
throw new ArgumentException($"Test argument exception: {message}");
}

public async Task Invoke_ThrowsAsync_InvalidOperationException()
{
await Task.Delay(10);
UpdateLastMethodCalled();
throw new InvalidOperationException("Test async invalid operation exception message");
}

public ComputationResult Invoke_ThrowsWithReturnType()
{
UpdateLastMethodCalled();
throw new NotSupportedException("Method with return type that throws exception");
}

public void Invoke_ThrowsCustomException()
{
UpdateLastMethodCalled();
throw new TestCustomException("Custom exception for testing", 42);
}

// Simple demo methods
public string GetSuccessMessage()
{
UpdateLastMethodCalled();
return "Method called successfully!";
}

public string ThrowTestException(string parameter)
{
UpdateLastMethodCalled();
throw new InvalidOperationException($"This is a test exception with parameter: {parameter}");
}

private void UpdateLastMethodCalled([CallerMemberName] string methodName = null)
{
LastMethodCalled = methodName;
}
}

// Custom exception class for testing
private class TestCustomException : Exception
{
public int ErrorCode { get; }

public TestCustomException(string message, int errorCode) : base(message)
{
ErrorCode = errorCode;
}
}

private class InvokeJavaScriptAsyncTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
Expand Down Expand Up @@ -657,6 +783,24 @@ IEnumerator IEnumerable.GetEnumerator()
}
}

// Test data for exception scenarios
private class InvokeDotNetExceptionTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] { "Invoke_ThrowsInvalidOperationException", "InvalidOperationException", "Test invalid operation exception message" };
yield return new object[] { "Invoke_ThrowsArgumentException", "ArgumentException", "Test argument exception: test_param" };
yield return new object[] { "Invoke_ThrowsAsync_InvalidOperationException", "InvalidOperationException", "Test async invalid operation exception message" };
yield return new object[] { "Invoke_ThrowsWithReturnType", "NotSupportedException", "Method with return type that throws exception" };
yield return new object[] { "Invoke_ThrowsCustomException", "TestCustomException", "Custom exception for testing" };
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

public class ComputationResult
{
public decimal result { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>Exception Handling Test</title>
<script src="scripts/HybridWebView.js"></script>
</head>
<body>
<h1>HybridWebView Exception Handling Test</h1>

<button id="testSuccess" onclick="testSuccessfulCall()">Test Successful Call</button>
<button id="testException" onclick="testExceptionCall()">Test Exception Call</button>

<div id="results"></div>

<script>
async function testSuccessfulCall() {
try {
const result = await window.HybridWebView.InvokeDotNet('GetSuccessMessage');
document.getElementById('results').innerHTML = '<p style="color: green;">Success: ' + result + '</p>';
} catch (error) {
document.getElementById('results').innerHTML = '<p style="color: red;">Unexpected error: ' + error.message + '</p>';
}
}

async function testExceptionCall() {
try {
const result = await window.HybridWebView.InvokeDotNet('ThrowTestException', 'test parameter');
document.getElementById('results').innerHTML = '<p style="color: red;">ERROR: Should have thrown exception but got: ' + result + '</p>';
} catch (error) {
document.getElementById('results').innerHTML =
'<div style="color: orange;">' +
'<p><strong>Exception caught successfully!</strong></p>' +
'<p>Type: ' + (error.dotNetType || 'JavaScript Error') + '</p>' +
'<p>Message: ' + error.message + '</p>' +
'<p>Stack trace available: ' + (error.dotNetStackTrace ? 'Yes' : 'No') + '</p>' +
'</div>';
}
}
</script>

<div id="htmlLoaded"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!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="scripts/HybridWebView.js"></script>
<script>
window.addEventListener(
"HybridWebViewMessageReceived",
async function (e) {
var messageData = e.detail.message.split('|');
var methodToInvoke = messageData[0];
var parameter = messageData.length > 1 ? messageData[1] : null;

var errorInfo = null;
var stackTrace = null;

try {
// Invoke the .NET method that should throw an exception
var result;
if (parameter) {
result = await window.HybridWebView.InvokeDotNet(methodToInvoke, parameter);
} else {
result = await window.HybridWebView.InvokeDotNet(methodToInvoke);
}

// If we get here, the exception wasn't thrown properly
errorInfo = "ERROR: Expected exception but method completed successfully with result: " + (result === null ? 'null' : JSON.stringify(result));
} catch (error) {
// This is expected - the .NET method should throw an exception
console.log('Caught expected exception from .NET:', error);

// Store error information for test verification
errorInfo = JSON.stringify({
message: error.message,
dotNetType: error.dotNetType,
dotNetStackTrace: error.dotNetStackTrace
});

stackTrace = error.dotNetStackTrace;
}

// Store the error info for the test to verify
lastErrorInfo = errorInfo;
lastErrorStackTrace = stackTrace;

SetStatusDone();
});

var lastErrorInfo = null;
var lastErrorStackTrace = null;

function GetLastErrorInfo() {
return lastErrorInfo;
}

function GetLastErrorStackTrace() {
return lastErrorStackTrace;
}

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>
19 changes: 19 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,25 @@
if (!response) {
return null;
}
// Check if the response contains an error
if (response.Error) {
const error = response.Error;
// Log the error to the browser console for debugging
console.error('Error invoking .NET method:', error);

// Create and throw a JavaScript Error with the .NET exception details
const jsError = new Error(error.Message || 'An error occurred while invoking a .NET method');

// Add additional information to the error object for debugging
if (error.Type) {
jsError.dotNetType = error.Type;
}
if (error.StackTrace) {
jsError.dotNetStackTrace = error.StackTrace;
}

throw jsError;
}
if (response.IsJson) {
return JSON.parse(response.Result);
}
Expand Down
26 changes: 26 additions & 0 deletions src/Core/src/Handlers/HybridWebView/HybridWebView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ interface JSInvokeError {
interface DotNetInvokeResult {
IsJson: boolean;
Result: any;
Error?: DotNetInvokeError;
}
interface DotNetInvokeError {
Type?: string;
Message: string;
StackTrace?: string;
}

(() => {
Expand Down Expand Up @@ -215,6 +221,26 @@ interface DotNetInvokeResult {
return null;
}

// Check if the response contains an error
if (response.Error) {
const error = response.Error;
// Log the error to the browser console for debugging
console.error('Error invoking .NET method:', error);

// Create and throw a JavaScript Error with the .NET exception details
const jsError = new Error(error.Message || 'An error occurred while invoking a .NET method');

// Add additional information to the error object for debugging
if (error.Type) {
(jsError as any).dotNetType = error.Type;
}
if (error.StackTrace) {
(jsError as any).dotNetStackTrace = error.StackTrace;
}

throw jsError;
}

if (response.IsJson) {
return JSON.parse(response.Result);
}
Expand Down
Loading