From edf59d580b4260ead73e5254544b254f2fa2f9b3 Mon Sep 17 00:00:00 2001
From: "coderabbitai[bot]"
<136622811+coderabbitai[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 09:05:29 +0000
Subject: [PATCH] CodeRabbit Generated Unit Tests: Add 27 new unit tests for
JavaScript callbacks and bindings
---
.../Javascript/JavascriptCallbackTests.cs | 287 ++++++++++++++
.../JavascriptBindingTests.cs | 365 ++++++++++++++++++
2 files changed, 652 insertions(+)
diff --git a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs
index e87e0a9ff..0a2237e59 100644
--- a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs
+++ b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs
@@ -321,5 +321,292 @@ public async Task ShouldWorkWhenExecutedMultipleTimes()
Assert.Equal(42, callbackResponse.Result);
}
}
+
+ [Fact]
+ public async Task ShouldHandleCallbackAfterMultipleContextChanges()
+ {
+ AssertInitialLoadComplete();
+
+ // Test that callbacks are properly cleaned up after multiple context changes
+ var javascriptResponse1 = await Browser.EvaluateScriptAsync("(function() { return Promise.resolve(42); })");
+ Assert.True(javascriptResponse1.Success);
+ var callback1 = (IJavascriptCallback)javascriptResponse1.Result;
+
+ // Change context
+ await Browser.LoadUrlAsync(CefExample.HelloWorldUrl);
+
+ var javascriptResponse2 = await Browser.EvaluateScriptAsync("(function() { return Promise.resolve(84); })");
+ Assert.True(javascriptResponse2.Success);
+ var callback2 = (IJavascriptCallback)javascriptResponse2.Result;
+
+ // Execute the new callback - should work
+ var callbackResponse2 = await callback2.ExecuteAsync();
+ Assert.True(callbackResponse2.Success);
+ Assert.Equal(84, callbackResponse2.Result);
+
+ // Old callback should fail gracefully
+ var callbackResponse1 = await callback1.ExecuteAsync();
+ Assert.False(callbackResponse1.Success);
+ Assert.Contains("Frame with Id:", callbackResponse1.Message);
+ }
+
+ [Fact]
+ public async Task ShouldProperlyCleanupCallbacksOnFrameDestruction()
+ {
+ using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false))
+ {
+ await browser.CreateBrowserAsync();
+ await browser.LoadUrlAsync(CefExample.HelloWorldUrl);
+
+ var javascriptResponse = await browser.EvaluateScriptAsync("(function() { return Promise.resolve('test'); })");
+ Assert.True(javascriptResponse.Success);
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+ var frameId = browser.GetMainFrame().Identifier;
+
+ // Execute callback successfully first
+ var result1 = await callback.ExecuteAsync();
+ Assert.True(result1.Success);
+ Assert.Equal("test", result1.Result);
+
+ // Load new page to destroy frame
+ await browser.LoadUrlAsync("about:blank");
+
+ // Callback should now fail with frame-specific error
+ var result2 = await callback.ExecuteAsync();
+ Assert.False(result2.Success);
+ Assert.Contains($"Frame with Id:{frameId}", result2.Message);
+ }
+ }
+
+ [Fact]
+ public async Task ShouldHandleCallbacksFromDifferentFrames()
+ {
+ using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false))
+ {
+ await browser.CreateBrowserAsync();
+
+ // Load a page with iframe
+ await browser.LoadHtmlAsync(@"
+
+
+ Main Frame
+
+
+ ");
+
+ // Create callback in main frame
+ var mainFrameResponse = await browser.EvaluateScriptAsync("(function() { return Promise.resolve('main'); })");
+ Assert.True(mainFrameResponse.Success);
+ var mainCallback = (IJavascriptCallback)mainFrameResponse.Result;
+
+ // Execute main frame callback
+ var mainResult = await mainCallback.ExecuteAsync();
+ Assert.True(mainResult.Success);
+ Assert.Equal("main", mainResult.Result);
+ }
+ }
+
+ [Theory]
+ [InlineData("(function() { return Promise.resolve(null); })", null)]
+ [InlineData("(function() { return Promise.resolve(undefined); })", null)]
+ public async Task ShouldHandleNullAndUndefinedCallbackResults(string script, object expected)
+ {
+ AssertInitialLoadComplete();
+
+ var javascriptResponse = await Browser.EvaluateScriptAsync(script);
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+ var callbackResponse = await callback.ExecuteAsync();
+
+ Assert.True(callbackResponse.Success);
+ Assert.Equal(expected, callbackResponse.Result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleNestedCallbackExecution()
+ {
+ AssertInitialLoadComplete();
+
+ // Create a callback that returns another function
+ var javascriptResponse = await Browser.EvaluateScriptAsync(@"
+ (function() {
+ return function(x) {
+ return Promise.resolve(x * 2);
+ };
+ })");
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+
+ // Execute with parameter
+ var callbackResponse = await callback.ExecuteAsync(21);
+ Assert.True(callbackResponse.Success);
+ Assert.Equal(42, callbackResponse.Result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleCallbackExecutionWithComplexObjects()
+ {
+ AssertInitialLoadComplete();
+
+ var javascriptResponse = await Browser.EvaluateScriptAsync(@"
+ (function(obj) {
+ return Promise.resolve({
+ doubled: obj.value * 2,
+ message: 'Result: ' + obj.value
+ });
+ })");
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+
+ var inputObj = new { value = 42 };
+ var callbackResponse = await callback.ExecuteAsync(inputObj);
+
+ Assert.True(callbackResponse.Success);
+ dynamic result = callbackResponse.Result;
+ Assert.Equal(84, (int)result.doubled);
+ Assert.Equal("Result: 42", (string)result.message);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(5)]
+ [InlineData(10)]
+ public async Task ShouldHandleMultipleSequentialCallbackExecutions(int executionCount)
+ {
+ AssertInitialLoadComplete();
+
+ var javascriptResponse = await Browser.EvaluateScriptAsync(@"
+ (function(x) {
+ return Promise.resolve(x + 1);
+ })");
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+
+ for (var i = 0; i < executionCount; i++)
+ {
+ var callbackResponse = await callback.ExecuteAsync(i);
+ Assert.True(callbackResponse.Success);
+ Assert.Equal(i + 1, callbackResponse.Result);
+ }
+ }
+
+ [Fact]
+ public async Task ShouldHandleCallbackWithLongRunningOperation()
+ {
+ AssertInitialLoadComplete();
+
+ var javascriptResponse = await Browser.EvaluateScriptAsync(@"
+ (function() {
+ return new Promise(resolve => {
+ setTimeout(() => resolve('completed'), 2000);
+ });
+ })");
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+
+ var callbackResponse = await callback.ExecuteAsync();
+ Assert.True(callbackResponse.Success);
+ Assert.Equal("completed", callbackResponse.Result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleCallbackErrorsGracefully()
+ {
+ AssertInitialLoadComplete();
+
+ var javascriptResponse = await Browser.EvaluateScriptAsync(@"
+ (function() {
+ return Promise.reject(new Error('Custom error message'));
+ })");
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+ var callbackResponse = await callback.ExecuteAsync();
+
+ Assert.False(callbackResponse.Success);
+ Assert.Contains("Custom error message", callbackResponse.Message);
+ }
+
+ [Fact]
+ public async Task ShouldVerifyCallbackRegistryCleanup()
+ {
+ // Test that callbacks are properly cleaned up when context is released
+ using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false))
+ {
+ await browser.CreateBrowserAsync();
+ await browser.LoadUrlAsync(CefExample.HelloWorldUrl);
+
+ var callbacks = new List();
+
+ // Create multiple callbacks
+ for (int i = 0; i < 5; i++)
+ {
+ var response = await browser.EvaluateScriptAsync($"(function() {{ return Promise.resolve({i}); }})");
+ Assert.True(response.Success);
+ callbacks.Add((IJavascriptCallback)response.Result);
+ }
+
+ // Verify all callbacks work
+ for (int i = 0; i < callbacks.Count; i++)
+ {
+ var result = await callbacks[i].ExecuteAsync();
+ Assert.True(result.Success);
+ Assert.Equal(i, result.Result);
+ }
+
+ // Destroy context
+ await browser.LoadUrlAsync("about:blank");
+
+ // Verify all callbacks are now invalid
+ foreach (var callback in callbacks)
+ {
+ var result = await callback.ExecuteAsync();
+ Assert.False(result.Success);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task ShouldHandleCallbackWithArrayParameter()
+ {
+ AssertInitialLoadComplete();
+
+ var javascriptResponse = await Browser.EvaluateScriptAsync(@"
+ (function(arr) {
+ return Promise.resolve(arr.reduce((a, b) => a + b, 0));
+ })");
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+ var callbackResponse = await callback.ExecuteAsync(new[] { 1, 2, 3, 4, 5 });
+
+ Assert.True(callbackResponse.Success);
+ Assert.Equal(15, callbackResponse.Result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleCallbackReturningArray()
+ {
+ AssertInitialLoadComplete();
+
+ var javascriptResponse = await Browser.EvaluateScriptAsync(@"
+ (function() {
+ return Promise.resolve([1, 2, 3, 4, 5]);
+ })");
+ Assert.True(javascriptResponse.Success);
+
+ var callback = (IJavascriptCallback)javascriptResponse.Result;
+ var callbackResponse = await callback.ExecuteAsync();
+
+ Assert.True(callbackResponse.Success);
+ var resultArray = callbackResponse.Result as object[];
+ Assert.NotNull(resultArray);
+ Assert.Equal(5, resultArray.Length);
+ }
}
}
diff --git a/CefSharp.Test/JavascriptBinding/JavascriptBindingTests.cs b/CefSharp.Test/JavascriptBinding/JavascriptBindingTests.cs
index 3bfbdd13d..3f57046ac 100644
--- a/CefSharp.Test/JavascriptBinding/JavascriptBindingTests.cs
+++ b/CefSharp.Test/JavascriptBinding/JavascriptBindingTests.cs
@@ -236,5 +236,370 @@ public async Task ShouldFireResolveObjectForUnregisteredObject()
Assert.NotNull(evt);
Assert.Equal("second", evt.Arguments.ObjectName);
}
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncWithMultipleObjects()
+ {
+ AssertInitialLoadComplete();
+
+ const string script = @"
+ (async function()
+ {
+ await CefSharp.BindObjectAsync('bound1', 'bound2');
+ var result1 = await bound1.echo('test1');
+ var result2 = await bound2.echo('test2');
+ return result1 + '|' + result2;
+ })();";
+
+ var boundObj1 = new BindingTestObject();
+ var boundObj2 = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("bound1", boundObj1);
+ Browser.JavascriptObjectRepository.Register("bound2", boundObj2);
+#else
+ Browser.JavascriptObjectRepository.Register("bound1", boundObj1, true);
+ Browser.JavascriptObjectRepository.Register("bound2", boundObj2, true);
+#endif
+
+ var result = await Browser.EvaluateScriptAsync(script);
+
+ Assert.Equal(1, boundObj1.EchoMethodCallCount);
+ Assert.Equal(1, boundObj2.EchoMethodCallCount);
+ Assert.Equal("test1|test2", result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncWithCachedObjects()
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("cached", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("cached", boundObj, true);
+#endif
+
+ // First bind - should cache
+ await Browser.EvaluateScriptAsync("CefSharp.BindObjectAsync('cached');");
+
+ // Second bind - should use cache
+ const string script = @"
+ (async function()
+ {
+ await CefSharp.BindObjectAsync('cached');
+ return await cached.echo('from cache');
+ })();";
+
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.Equal("from cache", result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncWithIgnoreCacheOption()
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("testobj", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("testobj", boundObj, true);
+#endif
+
+ // Bind with ignoreCache option
+ const string script = @"
+ (async function()
+ {
+ await CefSharp.BindObjectAsync({IgnoreCache: true}, 'testobj');
+ return await testobj.echo('no cache');
+ })();";
+
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.Equal("no cache", result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncWithNotifyIfAlreadyBound()
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("notify", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("notify", boundObj, true);
+#endif
+
+ // First bind
+ await Browser.EvaluateScriptAsync("CefSharp.BindObjectAsync('notify');");
+
+ // Try to bind again with notifyIfAlreadyBound
+ const string script = @"
+ (async function()
+ {
+ var result = await CefSharp.BindObjectAsync({NotifyIfAlreadyBound: true}, 'notify');
+ return result.Success;
+ })();";
+
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncAfterMultipleNavigations()
+ {
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("persistent", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("persistent", boundObj, true);
+#endif
+
+ const string script = @"
+ (async function()
+ {
+ await CefSharp.BindObjectAsync('persistent');
+ return await persistent.echo('test');
+ })();";
+
+ // Navigate and bind multiple times
+ for (int i = 0; i < 3; i++)
+ {
+ await Browser.LoadUrlAsync(CefExample.HelloWorldUrl);
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.Equal("test", result);
+ }
+
+ Assert.Equal(3, boundObj.EchoMethodCallCount);
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncWithMixedRegisteredAndUnregistered()
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj1 = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("registered", boundObj1);
+#else
+ Browser.JavascriptObjectRepository.Register("registered", boundObj1, true);
+#endif
+
+ // Try to bind both registered and unregistered object
+ var objRepository = Browser.JavascriptObjectRepository;
+
+ var evt = await Assert.RaisesAsync(
+ a => objRepository.ResolveObject += a,
+ a => objRepository.ResolveObject -= a,
+ () => Browser.EvaluateScriptAsync("CefSharp.BindObjectAsync('registered', 'unregistered');"));
+
+ Assert.NotNull(evt);
+ Assert.Equal("unregistered", evt.Arguments.ObjectName);
+ }
+
+ [Theory]
+ [InlineData("CefSharp.BindObjectAsync()")]
+ [InlineData("cefSharp.bindObjectAsync()")]
+ public async Task ShouldHandleBindObjectAsyncWithBothCasingVariants(string bindScript)
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("casingtest", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("casingtest", boundObj, true);
+#endif
+
+ var script = $@"
+ (async function()
+ {{
+ await {bindScript};
+ return true;
+ }})();";
+
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task ShouldVerifyJavascriptRootObjectWrapperIsNotNull()
+ {
+ // This test verifies the fix where _javascriptRootObjectWrapper should not be null
+ AssertInitialLoadComplete();
+
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("roottest", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("roottest", boundObj, true);
+#endif
+
+ // This should not throw an exception about null _javascriptRootObjectWrapper
+ const string script = @"
+ (async function()
+ {
+ await CefSharp.BindObjectAsync('roottest');
+ return await roottest.echo('success');
+ })();";
+
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.Equal("success", result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncInIframe()
+ {
+ using (var browser = new ChromiumWebBrowser(automaticallyCreateBrowser: false))
+ {
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ browser.JavascriptObjectRepository.Register("iframeobj", boundObj);
+#else
+ browser.JavascriptObjectRepository.Register("iframeobj", boundObj, true);
+#endif
+
+ browser.CreateBrowser();
+
+ await browser.LoadHtmlAsync(@"
+
+
+ Main Frame
+
+
+ ");
+
+ // Bind in main frame
+ const string script = @"
+ (async function()
+ {
+ await CefSharp.BindObjectAsync('iframeobj');
+ return await iframeobj.echo('main frame');
+ })();";
+
+ var result = await browser.EvaluateScriptAsync(script);
+ Assert.Equal("main frame", result);
+ }
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncWithEmptyObjectList()
+ {
+ AssertInitialLoadComplete();
+
+ // Bind with no objects specified
+ var result = await Browser.EvaluateScriptAsync("CefSharp.BindObjectAsync()");
+ Assert.True(result.Success);
+ }
+
+ [Fact]
+ public async Task ShouldHandleBindObjectAsyncWithConfigurationObject()
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("configtest", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("configtest", boundObj, true);
+#endif
+
+ // Bind with configuration object
+ const string script = @"
+ (async function()
+ {
+ var config = {
+ NotifyIfAlreadyBound: false,
+ IgnoreCache: false
+ };
+ await CefSharp.BindObjectAsync(config, 'configtest');
+ return await configtest.echo('configured');
+ })();";
+
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.Equal("configured", result);
+ }
+
+ [Fact]
+ public async Task ShouldHandleConcurrentBindObjectAsyncCalls()
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj1 = new BindingTestObject();
+ var boundObj2 = new BindingTestObject();
+ var boundObj3 = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("concurrent1", boundObj1);
+ Browser.JavascriptObjectRepository.Register("concurrent2", boundObj2);
+ Browser.JavascriptObjectRepository.Register("concurrent3", boundObj3);
+#else
+ Browser.JavascriptObjectRepository.Register("concurrent1", boundObj1, true);
+ Browser.JavascriptObjectRepository.Register("concurrent2", boundObj2, true);
+ Browser.JavascriptObjectRepository.Register("concurrent3", boundObj3, true);
+#endif
+
+ // Execute multiple bind operations concurrently
+ var task1 = Browser.EvaluateScriptAsync(@"
+ (async function() {
+ await CefSharp.BindObjectAsync('concurrent1');
+ return await concurrent1.echo('1');
+ })();");
+
+ var task2 = Browser.EvaluateScriptAsync(@"
+ (async function() {
+ await CefSharp.BindObjectAsync('concurrent2');
+ return await concurrent2.echo('2');
+ })();");
+
+ var task3 = Browser.EvaluateScriptAsync(@"
+ (async function() {
+ await CefSharp.BindObjectAsync('concurrent3');
+ return await concurrent3.echo('3');
+ })();");
+
+ await Task.WhenAll(task1, task2, task3);
+
+ Assert.Equal("1", await task1);
+ Assert.Equal("2", await task2);
+ Assert.Equal("3", await task3);
+ }
+
+ [Theory]
+ [InlineData("bindObjectAsync")]
+ [InlineData("BindObjectAsync")]
+ public async Task ShouldSupportCamelCaseAndPascalCaseBindMethods(string methodName)
+ {
+ AssertInitialLoadComplete();
+
+ var boundObj = new BindingTestObject();
+
+#if NETCOREAPP
+ Browser.JavascriptObjectRepository.Register("casetest", boundObj);
+#else
+ Browser.JavascriptObjectRepository.Register("casetest", boundObj, true);
+#endif
+
+ var script = $@"
+ (async function()
+ {{
+ await CefSharp.{methodName}('casetest');
+ return await casetest.echo('works');
+ }})();";
+
+ var result = await Browser.EvaluateScriptAsync(script);
+ Assert.Equal("works", result);
+ }
}
}