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); + } } }