diff --git a/.github/workflows/shared-testing-linux.yml b/.github/workflows/shared-testing-linux.yml index 1010bb752..fa3b2306a 100644 --- a/.github/workflows/shared-testing-linux.yml +++ b/.github/workflows/shared-testing-linux.yml @@ -273,17 +273,6 @@ jobs: framework_exit=0 dotnet "${test_args[@]}" 2>&1 | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | tee "artifacts/testresults/${framework}-output.log" || framework_exit=$? - # WebKitGTK emits SIGABRT (exit 134) during process teardown on - # Linux after all tests pass. This is a known race between WebKit - # web-process cleanup and display-server teardown. Treat it as - # success when every test assertion passed (failed: 0 in output). - if [ $framework_exit -eq 134 ]; then - if grep -qE '^\s*failed:\s+0\s*$' "artifacts/testresults/${framework}-output.log" 2>/dev/null; then - echo "WARN: SIGABRT during teardown with all tests passing (WebKitGTK cleanup race) β€” ignoring" - framework_exit=0 - fi - fi - if [ $framework_exit -ne 0 ]; then exit_code=$framework_exit fi diff --git a/examples/InfiniFrameExample.BlazorWebView/Program.cs b/examples/InfiniFrameExample.BlazorWebView/Program.cs index 6fb0fd100..5ef30cc8a 100644 --- a/examples/InfiniFrameExample.BlazorWebView/Program.cs +++ b/examples/InfiniFrameExample.BlazorWebView/Program.cs @@ -16,23 +16,21 @@ namespace InfiniFrameExample.BlazorWebView; public static class Program { [STAThread] private static void Main(string[] args) { - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args); + InfiniFrameApplication app = InfiniFrameApplication.Initialize(); + app.WithBlazorWebView(blazorBuilder => { + blazorBuilder.Services.AddLogging(config => { + config.ClearProviders(); + config.AddSerilog(); + }); - appBuilder.Services.AddLogging(config => { - config.ClearProviders(); - config.AddSerilog(); - }); - - appBuilder.Services.AddSerilog(config => { - config.WriteTo.Async(static c => c.Console()) - .MinimumLevel.Debug(); - }); + blazorBuilder.Services.AddSerilog(config => { + config.WriteTo.Async(static c => c.Console()) + .MinimumLevel.Debug(); + }); - // register the root component and selector - appBuilder.RootComponents.Add("app"); + blazorBuilder.RootComponents.Add("app"); - appBuilder.WithInfiniFrameWindowBuilder(builder => { - builder + blazorBuilder.WindowBuilder // .SetTransparent(true) // .SetChromeless(true) // .SetResizable(true) @@ -49,8 +47,6 @@ private static void Main(string[] args) { ; }); - InfiniFrameBlazorApp app = appBuilder.Build(); - app.Run(); } -} \ No newline at end of file +} diff --git a/examples/InfiniFrameExample.NativeMenu/Program.cs b/examples/InfiniFrameExample.NativeMenu/Program.cs index e7812b714..f8b0fd836 100644 --- a/examples/InfiniFrameExample.NativeMenu/Program.cs +++ b/examples/InfiniFrameExample.NativeMenu/Program.cs @@ -71,48 +71,49 @@ public static void Main(string[] args) { ] ); - IInfiniFrameWindow window = InfiniFrameWindowBuilder.Create() - .SetTitle("InfiniFrame Native Menu Example") - .SetSize(new Size(960, 640)) - .CenteredOnMainMonitor() - .SetMenuBar(menuBar) - .UseEmbeddedWwwrootAssets( - scheme: "app", - includePhysicalFallback: true, - physicalWwwrootPath: Path.Join(AppContext.BaseDirectory, "wwwroot"), - setStartUrl: true - ) - .RegisterWebMessageReceivedHandler((win, message) => { - string? action = ExtractAction(message); - if (action == null) return; + var app = InfiniFrameApplication.Initialize() + .WithWindow(builder => builder + .SetTitle("InfiniFrame Native Menu Example") + .SetSize(new Size(960, 640)) + .CenteredOnMainMonitor() + .SetMenuBar(menuBar) + .UseEmbeddedWwwrootAssets( + scheme: "app", + includePhysicalFallback: true, + physicalWwwrootPath: Path.Join(AppContext.BaseDirectory, "wwwroot"), + setStartUrl: true + ) + .RegisterWebMessageReceivedHandler((win, message) => { + string? action = ExtractAction(message); + if (action == null) return; - switch (action) { - case "enable-save": - win.Features.Menu.SetMenuItemEnabled("file-save", true); - win.SendWebMessage("status:Save enabled"); - break; + switch (action) { + case "enable-save": + win.Features.Menu.SetMenuItemEnabled("file-save", true); + win.SendWebMessage("status:Save enabled"); + break; - case "disable-save": - win.Features.Menu.SetMenuItemEnabled("file-save", false); - win.SendWebMessage("status:Save disabled"); - break; + case "disable-save": + win.Features.Menu.SetMenuItemEnabled("file-save", false); + win.SendWebMessage("status:Save disabled"); + break; - case "toggle-undo": - bool undoVisible = win.Features.Menu.MenuBar.Items - .First(i => i.Id == "edit").Children - .First(i => i.Id == "edit-undo").IsVisible; - win.Features.Menu.SetMenuItemVisible("edit-undo", !undoVisible); - win.SendWebMessage($"status:Undo {(undoVisible ? "hidden" : "shown")}"); - break; + case "toggle-undo": + bool undoVisible = win.Features.Menu.MenuBar.Items + .First(i => i.Id == "edit").Children + .First(i => i.Id == "edit-undo").IsVisible; + win.Features.Menu.SetMenuItemVisible("edit-undo", !undoVisible); + win.SendWebMessage($"status:Undo {(undoVisible ? "hidden" : "shown")}"); + break; - default: - win.SendWebMessage($"status:Action: {action}"); - break; - } - }) - .Build(); + default: + win.SendWebMessage($"status:Action: {action}"); + break; + } + }) + ); - window.WaitForClose(); + app.Run(); } private static string? ExtractAction(string rawMessage) { diff --git a/examples/InfiniFrameExample.TrimAotSmoke/Program.cs b/examples/InfiniFrameExample.TrimAotSmoke/Program.cs index 9e413fde6..f57b98b09 100644 --- a/examples/InfiniFrameExample.TrimAotSmoke/Program.cs +++ b/examples/InfiniFrameExample.TrimAotSmoke/Program.cs @@ -11,13 +11,15 @@ namespace InfiniFrameExample.TrimAotSmoke; public static class Program { [STAThread] public static void Main() { - IInfiniFrameWindow window = InfiniFrameWindowBuilder.Create() - .SetTitle("InfiniFrame Trim/AOT Smoke") - .SetSize(800, 600) - .CenteredOnMainMonitor() - .UseEmbeddedWwwrootAssets() - .Build(); + InfiniFrameApplication app = InfiniFrameApplication.Initialize() + .WithWindow(builder => { + builder + .SetTitle("InfiniFrame Trim/AOT Smoke") + .SetSize(800, 600) + .CenteredOnMainMonitor() + .UseEmbeddedWwwrootAssets(); + }); - window.WaitForClose(); + app.Run(); } -} \ No newline at end of file +} diff --git a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.MudBlazor/Program.cs b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.MudBlazor/Program.cs index 09b29ccad..51c9c82ac 100644 --- a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.MudBlazor/Program.cs +++ b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.MudBlazor/Program.cs @@ -25,32 +25,30 @@ private static void Main(string[] args) { try { Log.Information("Starting InfiniFrame MudBlazor example..."); - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args); - - appBuilder.Services - .AddLogging(config => { - config.ClearProviders(); - config.AddSerilog(); - }) - .AddSerilog(config => { - config.WriteTo.Async(static c => c.Console()) - .MinimumLevel.Debug(); - }) - .AddMudServices(); - - appBuilder.RootComponents.Add("app"); - - appBuilder.WindowBuilder - .SetIconFile("wwwroot/favicon.ico") - .RegisterOpenExternalTargetWebMessageHandler(); - - InfiniFrameSingleFile.AddSingleFileRequirements(appBuilder); - - Log.Information("Building InfiniFrame application..."); - InfiniFrameBlazorApp application = appBuilder.Build(); + InfiniFrameApplication app = InfiniFrameApplication.Initialize(); + app.WithBlazorWebView(blazorBuilder => { + blazorBuilder.Services + .AddLogging(config => { + config.ClearProviders(); + config.AddSerilog(); + }) + .AddSerilog(config => { + config.WriteTo.Async(static c => c.Console()) + .MinimumLevel.Debug(); + }) + .AddMudServices(); + + blazorBuilder.RootComponents.Add("app"); + + blazorBuilder.WindowBuilder + .SetIconFile("wwwroot/favicon.ico") + .RegisterOpenExternalTargetWebMessageHandler(); + + blazorBuilder.AddSingleFileRequirements(); + }); Log.Information("Running application..."); - application.Run(); + app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); diff --git a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.React/Program.cs b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.React/Program.cs index e2817d90c..31b433f81 100644 --- a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.React/Program.cs +++ b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.React/Program.cs @@ -1,22 +1,28 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; using System.Drawing; using InfiniFrame.SingleFile; namespace InfiniFrameExample.SingleFileExe.React; - +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- public static class Program { [STAThread] public static void Main(string[] args) { InfiniFrameSingleFile.Initialize(); - IInfiniFrameWindowBuilder builder = InfiniFrameWindowBuilder.Create() - .SetTitle("InfiniFrame + React") - .SetSize(new Size(960, 640)) - .CenteredOnMainMonitor(); - - builder.AddSingleFileRequirements(); + InfiniFrameApplication app = InfiniFrameApplication.Initialize() + .WithWindow(builder => { + builder + .SetTitle("InfiniFrame + React") + .SetSize(new Size(960, 640)) + .CenteredOnMainMonitor() + .AddSingleFileRequirements(); + }); - IInfiniFrameWindow window = builder.Build(); - window.WaitForClose(); + app.Run(); } } diff --git a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.Vue/Program.cs b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.Vue/Program.cs index 866c60e20..e6128ea94 100644 --- a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.Vue/Program.cs +++ b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe.Vue/Program.cs @@ -14,14 +14,15 @@ public static class Program { public static void Main(string[] args) { InfiniFrameSingleFile.Initialize(); - IInfiniFrameWindowBuilder builder = InfiniFrameWindowBuilder.Create() - .SetTitle("InfiniFrame + Vue") - .SetSize(new Size(960, 640)) - .CenteredOnMainMonitor(); + InfiniFrameApplication app = InfiniFrameApplication.Initialize() + .WithWindow(builder => { + builder + .SetTitle("InfiniFrame + Vue") + .SetSize(new Size(960, 640)) + .CenteredOnMainMonitor() + .AddSingleFileRequirements(); + }); - builder.AddSingleFileRequirements(); - - IInfiniFrameWindow window = builder.Build(); - window.WaitForClose(); + app.Run(); } } diff --git a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe/Program.cs b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe/Program.cs index 10387ee05..1d8384662 100644 --- a/examples/SingleFileExe/InfiniFrameExample.SingleFileExe/Program.cs +++ b/examples/SingleFileExe/InfiniFrameExample.SingleFileExe/Program.cs @@ -14,15 +14,15 @@ public static class Program { public static void Main(string[] args) { InfiniFrameSingleFile.Initialize(); - IInfiniFrameWindowBuilder builder = InfiniFrameWindowBuilder.Create() - .SetTitle("InfiniFrame Embedded wwwroot") - .SetSize(new Size(960, 640)) - .CenteredOnMainMonitor(); - - builder.AddSingleFileRequirements(); - - IInfiniFrameWindow window = builder.Build(); + InfiniFrameApplication app = InfiniFrameApplication.Initialize() + .WithWindow(builder => { + builder + .SetTitle("InfiniFrame Embedded wwwroot") + .SetSize(new Size(960, 640)) + .CenteredOnMainMonitor() + .AddSingleFileRequirements(); + }); - window.WaitForClose(); + app.Run(); } } diff --git a/examples/WebApp/InfiniFrameExample.WebApp.Blazor/Program.cs b/examples/WebApp/InfiniFrameExample.WebApp.Blazor/Program.cs index 65969dbf3..a0e6a07e3 100644 --- a/examples/WebApp/InfiniFrameExample.WebApp.Blazor/Program.cs +++ b/examples/WebApp/InfiniFrameExample.WebApp.Blazor/Program.cs @@ -17,72 +17,75 @@ private static void Main(string[] args) { // ------------------------------------------------------------------------------------------------------------- // Builder // ------------------------------------------------------------------------------------------------------------- - InfiniFrameWebApplicationBuilder appBuilder = InfiniFrameWebApplication.CreateBuilder(args); + InfiniFrameApplication app = InfiniFrameApplication.Initialize(); + InfiniFrameWebApplication webApp = app.WithWebServer( + webAppBuilder => { + webAppBuilder.Services + .AddLogging(config => { + config.ClearProviders(); + config.AddSerilog(); + }) + .AddSerilog(config => { + config.WriteTo.Async(static c => c.Console()) + .MinimumLevel.Debug(); + }) + .AddRazorComponents() + .AddInteractiveServerComponents(); - appBuilder.Services - .AddLogging(config => { - config.ClearProviders(); - config.AddSerilog(); - }) - .AddSerilog(config => { - config.WriteTo.Async(static c => c.Console()) - .MinimumLevel.Debug(); - }) - .AddRazorComponents() - .AddInteractiveServerComponents(); + webAppBuilder.Services.AddHttpClient("ServerApi", (sp, client) => { + var config = sp.GetRequiredService(); - appBuilder.Services.AddHttpClient("ServerApi", (sp, client) => { - var config = sp.GetRequiredService(); + // Prefer ASPNETCORE_URLS, then "urls", then a fallback + string urls = config["ASPNETCORE_URLS"] + ?? config["urls"] + ?? "http://localhost:5000"; - // Prefer ASPNETCORE_URLS, then "urls", then a fallback - string urls = config["ASPNETCORE_URLS"] - ?? config["urls"] - ?? "http://localhost:5000"; + string baseUrl = urls + .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .First(); - string baseUrl = urls - .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .First(); + client.BaseAddress = new Uri(baseUrl); + }); + webAppBuilder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("ServerApi")); - client.BaseAddress = new Uri(baseUrl); - }); - appBuilder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("ServerApi")); + webAppBuilder.Services.AddInfiniFrameJs(); - appBuilder.Services.AddInfiniFrameJs(); - - appBuilder.WebApp.WebHost.UseStaticWebAssets(); - - appBuilder.WindowBuilder - // .SetTransparent(true) - // .SetChromeless(true) - // .SetResizable(true) - .SetIconFile("wwwroot/favicon.ico") - // .Center() - // .SetUseOsDefaultSize(true) - // .SetUseOsDefaultLocation(true); - // .SetTitle("InfiniLore InfiniFrame.Blazor Sample") - .SetLocation(new Point(100, 100)) - .SetSize(new Size(800, 600)) - .RegisterOpenExternalTargetWebMessageHandler() - // .SetMaxSize(new Size(800, 600)) - // .SetMinSize(new Size(600, 400)) - ; + webAppBuilder.WebHost.UseStaticWebAssets(); + }, + windowBuilder => { + windowBuilder + // .SetTransparent(true) + // .SetChromeless(true) + // .SetResizable(true) + .SetIconFile("wwwroot/favicon.ico") + // .Center() + // .SetUseOsDefaultSize(true) + // .SetUseOsDefaultLocation(true); + // .SetTitle("InfiniLore InfiniFrame.Blazor Sample") + .SetLocation(new Point(100, 100)) + .SetSize(new Size(800, 600)) + .RegisterOpenExternalTargetWebMessageHandler() + // .SetMaxSize(new Size(800, 600)) + // .SetMinSize(new Size(600, 400)) + ; + } + ); // ------------------------------------------------------------------------------------------------------------- // App // ------------------------------------------------------------------------------------------------------------- - InfiniFrameWebApplication application = appBuilder.Build(); - application.UseAutoServerClose(); + webApp.UseAutoServerClose(); - WebApplication webApp = application.WebApp; + WebApplication webAppInstance = webApp.WebApp; - webApp.UseRouting(); + webAppInstance.UseRouting(); - webApp.UseAntiforgery(); - webApp.MapStaticAssets(); + webAppInstance.UseAntiforgery(); + webAppInstance.MapStaticAssets(); - webApp.MapRazorComponents() + webAppInstance.MapRazorComponents() .AddInteractiveServerRenderMode(); - application.Run(); + app.Run(); } -} \ No newline at end of file +} diff --git a/examples/WebApp/InfiniFrameExample.WebApp.React/Program.cs b/examples/WebApp/InfiniFrameExample.WebApp.React/Program.cs index b32b42dcc..5b7b94d31 100644 --- a/examples/WebApp/InfiniFrameExample.WebApp.React/Program.cs +++ b/examples/WebApp/InfiniFrameExample.WebApp.React/Program.cs @@ -17,41 +17,43 @@ private sealed class WebMessageCounter { [STAThread] public static void Main(string[] args) { - InfiniFrameWebApplicationBuilder appBuilder = InfiniFrameWebApplication.CreateBuilder(args); - // WebApplicationBuilder appBuilder = builder.WebApp; - appBuilder.WebApp.Services.AddSingleton(); + InfiniFrameApplication app = InfiniFrameApplication.Initialize(); + InfiniFrameWebApplication webApp = app.WithWebServer( + webAppBuilder => { + webAppBuilder.Services.AddSingleton(); + }, + windowBuilder => { + windowBuilder + .UseOsDefaultSize(false) + .SetResizable() + .CenteredOnMainMonitor() + .SetTitle("InfiniLore InfiniFrame.NET REACT Sample") + .SetSize(new Size(800, 600)) + .RegisterCustomSchemeHandler("app", handler: (_, _) => ( + new MemoryStream([ + .. """ + (() =>{ + window.setTimeout(() => { + alert(`πŸŽ‰ Dynamically inserted JavaScript.`); + }, 1000); + })(); + """u8 + ]) + , "text/javascript") + ) + .RegisterWebMessageReceivedHandler((IInfiniFrameWindow window, string message, WebMessageCounter counter) => { + int count = counter.Increment(); + string response = $"[{count}] Received message: \"{message}\""; + window.SendWebMessage(response); + }); + } + ); - appBuilder.WindowBuilder - .UseOsDefaultSize(false) - .SetResizable() - .CenteredOnMainMonitor() - .SetTitle("InfiniLore InfiniFrame.NET REACT Sample") - .SetSize(new Size(800, 600)) - .RegisterCustomSchemeHandler("app", handler: (_, _) => ( - new MemoryStream([ - .. """ - (() =>{ - window.setTimeout(() => { - alert(`πŸŽ‰ Dynamically inserted JavaScript.`); - }, 1000); - })(); - """u8 - ]) - , "text/javascript") - ) - .RegisterWebMessageReceivedHandler((IInfiniFrameWindow window, string message, WebMessageCounter counter) => { - int count = counter.Increment(); - string response = $"[{count}] Received message: \"{message}\""; - window.SendWebMessage(response); - }); + webApp.UseAutoServerClose(); - InfiniFrameWebApplication application = appBuilder.Build(); + webApp.WebApp.UseStaticFiles(); + webApp.WebApp.MapStaticAssets(); - application.UseAutoServerClose(); - - application.WebApp.UseStaticFiles(); - application.WebApp.MapStaticAssets(); - - application.Run(); + app.Run(); } -} \ No newline at end of file +} diff --git a/examples/WebApp/InfiniFrameExample.WebApp.Vue/Program.cs b/examples/WebApp/InfiniFrameExample.WebApp.Vue/Program.cs index 50a6541e9..7c75d02e5 100644 --- a/examples/WebApp/InfiniFrameExample.WebApp.Vue/Program.cs +++ b/examples/WebApp/InfiniFrameExample.WebApp.Vue/Program.cs @@ -12,37 +12,38 @@ namespace InfiniFrameExample.WebApp.Vue; public static class Program { [STAThread] public static void Main(string[] args) { - InfiniFrameWebApplicationBuilder appBuilder = InfiniFrameWebApplication.CreateBuilder(args); - // WebApplicationBuilder appBuilder = builder.WebApp; - - if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux()) appBuilder.WindowBuilder.Debugging.SetRemoteDebuggingPort(9222); - - appBuilder.WindowBuilder - .CenteredOnMainMonitor() - // .SetTransparent(true) - // .SetUseOsDefaultSize(false) - .SetTitle("InfiniLore InfiniFrame.NET VUE Sample") - .SetSize(new Size(800, 600)) - .SetLocation(1000, 0) - .RegisterFullScreenWebMessageHandler() - .RegisterOpenExternalTargetWebMessageHandler() - .RegisterTitleChangedWebMessageHandler() - .RegisterWindowManagementWebMessageHandler() - .RegisterWebMessageReceivedHandler((_, message) => { - // ReSharper disable twice UnusedVariable - string response = $"Received message: \"{message}\""; - - // ... do something with the message - }) - ; - - InfiniFrameWebApplication application = appBuilder.Build(); - - application.UseAutoServerClose(); - - application.WebApp.UseStaticFiles(); - application.WebApp.MapStaticAssets(); - - application.Run(); + InfiniFrameApplication app = InfiniFrameApplication.Initialize(); + InfiniFrameWebApplication webApp = app.WithWebServer( + _ => { }, + windowBuilder => { + if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux()) windowBuilder.Debugging.SetRemoteDebuggingPort(9222); + + windowBuilder + .CenteredOnMainMonitor() + // .SetTransparent(true) + // .SetUseOsDefaultSize(false) + .SetTitle("InfiniLore InfiniFrame.NET VUE Sample") + .SetSize(new Size(800, 600)) + .SetLocation(1000, 0) + .RegisterFullScreenWebMessageHandler() + .RegisterOpenExternalTargetWebMessageHandler() + .RegisterTitleChangedWebMessageHandler() + .RegisterWindowManagementWebMessageHandler() + .RegisterWebMessageReceivedHandler((_, message) => { + // ReSharper disable twice UnusedVariable + string response = $"Received message: \"{message}\""; + + // ... do something with the message + }) + ; + } + ); + + webApp.UseAutoServerClose(); + + webApp.WebApp.UseStaticFiles(); + webApp.WebApp.MapStaticAssets(); + + app.Run(); } -} \ No newline at end of file +} diff --git a/examples/WebApp/InfiniFrameExample.WebApp/Program.cs b/examples/WebApp/InfiniFrameExample.WebApp/Program.cs index 8de617cc8..95c904417 100644 --- a/examples/WebApp/InfiniFrameExample.WebApp/Program.cs +++ b/examples/WebApp/InfiniFrameExample.WebApp/Program.cs @@ -14,17 +14,15 @@ namespace InfiniFrameExample.WebApp; public static class Program { [STAThread] public static void Main(string[] args) { - InfiniFrameWebApplicationBuilder builder = - InfiniFrameWebApplication.CreateBuilder(args); - - builder.WebApp.WebHost.UseUrls("http://127.0.0.1:5055"); - builder.WindowBuilder - .SetStartPageUrl("http://127.0.0.1:5055") - .SetTitle("InfiniFrame WebServer Repro") - .SetIconFile("wwwroot/favicon.ico"); - - InfiniFrameWebApplication app = builder.Build(); - app.UseAutoServerClose(); + var app = InfiniFrameApplication.Initialize() + .WithWebServer( + configureWebApp: webApp => { + webApp.WebHost.UseUrls("http://127.0.0.1:5055"); + }, + configureWindow: window => window + .SetTitle("InfiniFrame WebServer Repro") + .SetIconFile("wwwroot/favicon.ico") + ); app.WebApp.MapGet("/", handler: () => Results.Content( "InfiniFrame loaded", @@ -33,4 +31,4 @@ public static void Main(string[] args) { app.Run(); } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameApplicationBlazorExtensions.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameApplicationBlazorExtensions.cs new file mode 100644 index 000000000..8fc2d6f64 --- /dev/null +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameApplicationBlazorExtensions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using InfiniFrame.BlazorWebView; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace InfiniFrame; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Extension methods for integrating Blazor WebView with . +/// +public static class InfiniFrameApplicationBlazorExtensions { + /// + /// Adds a Blazor WebView to the application. The Blazor components are hosted inside + /// the native window's web view. + /// + /// The application instance. + /// Optional callback to configure the . + /// The for further configuration. + public static InfiniFrameBlazorApp WithBlazorWebView( + this InfiniFrameApplication app, + Action? configure = null + ) { + InfiniFrameBlazorAppBuilder blazorBuilder = new InfiniFrameBlazorAppBuilder(); + configure?.Invoke(blazorBuilder); + + // Merge Blazor-specific services into the application's service collection. + // This ensures all services (including IInfiniFrameApplication) resolve from + // the same provider when windows are built. + foreach (ServiceDescriptor descriptor in blazorBuilder.Services) { + app.ServiceCollection.Add(descriptor); + } + + // Register the window with the application. + string windowId = $"blazor-{app.Id}"; + app.WithWindow(windowId, blazorBuilder.WindowBuilder); + + // Build the Blazor app using the application's shared service provider. + IServiceProvider sharedProvider = app.ServiceProvider; + return blazorBuilder.Build(sharedProvider); + } +} diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs index b892c038f..647c26e98 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs @@ -23,43 +23,14 @@ public class InfiniFrameBlazorAppBuilder : IInfiniFrameBlazorAppBuilder { // ----------------------------------------------------------------------------------------------------------------- // Constructors // ----------------------------------------------------------------------------------------------------------------- - private InfiniFrameBlazorAppBuilder() {} - /// - public IInfiniFrameRootComponentList RootComponents { get; } = new InfiniFrameRootComponentList(); - /// - public IServiceCollection Services { get; } = new ServiceCollection(); - /// - public IInfiniFrameWindowBuilder WindowBuilder { get; } = InfiniFrameWindowBuilder.Create(); + public InfiniFrameBlazorAppBuilder() { + IFileProvider resolvedFileProvider = ConfigureFileProvider(null); - /// - /// Creates a default builder with standard configuration, command-line args, and window builder action. - /// - /// Optional command-line arguments. - /// An optional action to configure the window builder. - /// A new instance. - public static InfiniFrameBlazorAppBuilder CreateDefault( - string[]? args = null, - Action? windowBuilder = null - ) - => CreateDefault(null, args, windowBuilder); + Services.AddOptions(); - /// - /// Creates a default builder with standard configuration and command-line args. - /// - /// An optional file provider for static assets. - /// Optional command-line arguments. - /// An optional action to configure the window builder. - /// A new instance. - public static InfiniFrameBlazorAppBuilder CreateDefault(IFileProvider? fileProvider, string[]? args = null, Action? windowBuilder = null) { - // We don't use the args for anything right now, but we want to accept them - // here so that it shows up this way in the project templates. - var appBuilder = new InfiniFrameBlazorAppBuilder(); - IFileProvider resolvedFileProvider = ConfigureFileProvider(fileProvider); - - appBuilder.Services.AddOptions(); - - appBuilder.Services + Services .AddInfiniFrame() + .AddTransient() .AddScoped(static sp => { var handler = sp.GetRequiredService(); return new HttpClient(handler) { BaseAddress = new Uri(InfiniFrameWebViewManager.AppBaseUri) }; @@ -71,6 +42,7 @@ public static InfiniFrameBlazorAppBuilder CreateDefault(IFileProvider? fileProvi .AddSingleton() .AddSingleton() .AddSingleton(static provider => provider.GetRequiredService().Build(provider)) + .AddSingleton(WindowBuilder) .AddBlazorWebView() .AddSingleton(resolvedFileProvider) .AddSingleton(static provider => { @@ -83,19 +55,20 @@ public static InfiniFrameBlazorAppBuilder CreateDefault(IFileProvider? fileProvi DefaultDocument = NormalizeHostPage(config.HostPage) }; }) - .AddSingleton(appBuilder.WindowBuilder) - .AddSingleton(appBuilder.RootComponents) - .AddSingleton(appBuilder.RootComponents.JSComponents); - - appBuilder.Services.TryAddSingleton(); + .AddSingleton(RootComponents) + .AddSingleton(RootComponents.JSComponents); - appBuilder.Services.AddInfiniFrameJs(); - appBuilder.WindowBuilder.RegisterGetWebMessageHandler(); + Services.TryAddSingleton(); - windowBuilder?.Invoke(appBuilder.WindowBuilder); - - return appBuilder; + Services.AddInfiniFrameJs(); + WindowBuilder.RegisterGetWebMessageHandler(); } + /// + public IInfiniFrameRootComponentList RootComponents { get; } = new InfiniFrameRootComponentList(); + /// + public IServiceCollection Services { get; } = new ServiceCollection(); + /// + public IInfiniFrameWindowBuilder WindowBuilder { get; } = new InfiniFrameWindowBuilder(); /// /// Configures the file provider to be used by the application. diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs index 53c84ef58..261f42889 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs @@ -287,15 +287,17 @@ void ExecuteCallback() { } InfiniFrameDispatchResult result = LazyWindow.Value.Features.Invoke.Invoke(ExecuteCallback); - if (result == InfiniFrameDispatchResult.WindowClosed) { - // Renderer disposal is scheduled after the native window has closed. There is no UI thread left to - // dispatch to, but the serialized callback must still run or Blazor's DisposeAsync never completes. - ExecuteCallback(); - return; + switch (result) + { + case InfiniFrameDispatchResult.WindowClosed: + // Renderer disposal is scheduled after the native window has closed. There is no UI thread left to + // dispatch to, but the serialized callback must still run or Blazor's DisposeAsync never completes. + ExecuteCallback(); + return; + case InfiniFrameDispatchResult.Completed: + return; } - if (result == InfiniFrameDispatchResult.Completed) return; - Exception dispatchException = callbackException ?? new InvalidOperationException( $"Could not execute an InfiniFrame synchronization callback. Dispatch result: {result}." ); diff --git a/src/InfiniFrame.Js/package.json b/src/InfiniFrame.Js/package.json index 438c0c86f..bc8cd6ac4 100644 --- a/src/InfiniFrame.Js/package.json +++ b/src/InfiniFrame.Js/package.json @@ -1,6 +1,7 @@ { "name": "infinilore.infiniframe.js-build", "version": "0.1.0", + "type": "module", "description": "JavaScript/TypeScript interop library for InfiniFrame Blazor applications. Provides the client-side bridge between C# and the browser.", "author": "InfiniLore", "license": "Apache-2.0", diff --git a/src/InfiniFrame.Js/vite.config.dev.ts b/src/InfiniFrame.Js/vite.config.dev.ts index c7239d012..0b2809a93 100644 --- a/src/InfiniFrame.Js/vite.config.dev.ts +++ b/src/InfiniFrame.Js/vite.config.dev.ts @@ -1,7 +1,7 @@ ο»Ώimport {defineConfig} from "vite"; import {resolve} from "node:path"; -const entry = resolve(__dirname, "TypeScript/Index.ts"); +const entry = resolve(import.meta.dirname, "TypeScript/Index.ts"); export default defineConfig({ build: { diff --git a/src/InfiniFrame.Js/vite.config.prod.ts b/src/InfiniFrame.Js/vite.config.prod.ts index 23d36a9ee..ddeca7bb7 100644 --- a/src/InfiniFrame.Js/vite.config.prod.ts +++ b/src/InfiniFrame.Js/vite.config.prod.ts @@ -1,7 +1,7 @@ ο»Ώimport {defineConfig} from "vite"; import {resolve} from "node:path"; -const entry = resolve(__dirname, "TypeScript/Index.ts"); +const entry = resolve(import.meta.dirname, "TypeScript/Index.ts"); export default defineConfig({ build: { diff --git a/src/InfiniFrame.NativeBridge/Managed/ArtifactManifest.cs b/src/InfiniFrame.NativeBridge/Managed/ArtifactManifest.cs index efd48399a..abb06627d 100644 --- a/src/InfiniFrame.NativeBridge/Managed/ArtifactManifest.cs +++ b/src/InfiniFrame.NativeBridge/Managed/ArtifactManifest.cs @@ -17,7 +17,7 @@ public static class ArtifactManifest { public const string WindowsNativeFileName = $"{NativeLibraryName}.dll"; /// The logical name of the WebView2 loader library used on Windows. public const string WindowsLoaderLibraryName = "WebView2Loader"; - /// The Windows filename for the WebView2 loader library. + /// The Windows filename for the WebView2 loader library. public const string WindowsLoaderFileName = $"{WindowsLoaderLibraryName}.dll"; /// The Linux filename for the native library (e.g., InfiniFrame.Native.so). public const string LinuxNativeFileName = $"{NativeLibraryName}.so"; diff --git a/src/InfiniFrame.NativeBridge/Managed/Handles/NativeApplicationHandle.cs b/src/InfiniFrame.NativeBridge/Managed/Handles/NativeApplicationHandle.cs new file mode 100644 index 000000000..67c3c32a1 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Managed/Handles/NativeApplicationHandle.cs @@ -0,0 +1,23 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using Microsoft.Win32.SafeHandles; + +namespace InfiniFrame.NativeBridge.Handles; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Safe handle for a native InfiniFrameApplication instance. +/// +internal sealed class NativeApplicationHandle : SafeHandleZeroOrMinusOneIsInvalid { + internal NativeApplicationHandle(IntPtr handle) : base(ownsHandle: true) { + SetHandle(handle); + } + + /// + protected override bool ReleaseHandle() { + InfiniFrameNativeInteropStatus status = InfiniFrameNative.ApplicationDestructor(handle); + return status == InfiniFrameNativeInteropStatus.Success; + } +} diff --git a/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs b/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs index e065532d7..a48caa03f 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs @@ -9,6 +9,9 @@ namespace InfiniFrame.NativeBridge.Handles; // --------------------------------------------------------------------------------------------------------------------- /// Owns a native InfiniFrame window instance. public sealed class NativeWindowHandle : SafeHandleZeroOrMinusOneIsInvalid { + private volatile bool _safeToDestroy; + private int _released; + internal NativeWindowHandle(IntPtr handle) : base(true) { SetHandle(handle); } @@ -17,12 +20,23 @@ internal NativeWindowHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) { SetHandle(handle); } + internal void MarkSafeToDestroy() { + _safeToDestroy = true; + TryDestroy(); + } + protected override bool ReleaseHandle() { - InfiniFrameNative.Destructor(handle); + TryDestroy(); // Always return true to prevent SafeHandle finalizer from retrying a doomed destructor. // Logging is not available in finalizer context; the destructor status is observable // via the window lifecycle state if needed. return true; } + + private void TryDestroy() { + if (_safeToDestroy && Interlocked.CompareExchange(ref _released, 1, 0) == 0) { + InfiniFrameNative.Destructor(handle); + } + } } diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Application.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Application.cs new file mode 100644 index 000000000..149b506e4 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Application.cs @@ -0,0 +1,80 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace InfiniFrame.NativeBridge; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public partial class InfiniFrameNative { + /// + /// Creates a new native application instance with the specified parameters. + /// + /// The application initialization parameters. + /// The created native application instance handle. + /// A status code indicating success or failure. + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Application_ctor", SetLastError = true)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus ApplicationConstructor(IntPtr parameters, out IntPtr value); + + /// + /// Destroys the native application instance and releases its resources. + /// + /// The native application instance handle. + /// A status code indicating success or failure. + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Application_dtor")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus ApplicationDestructor(IntPtr instance); + + /// + /// Runs the application message loop, blocking until all windows close or Shutdown is called. + /// + /// The native application instance handle. + /// A status code indicating success or failure. + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Application_Run", SetLastError = true)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus ApplicationRun(IntPtr instance); + + /// + /// Signals the application message loop to exit. + /// + /// The native application instance handle. + /// A status code indicating success or failure. + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Application_Shutdown", SetLastError = true)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus ApplicationShutdown(IntPtr instance); + + /// + /// Checks if Shutdown has been called on the application. + /// + /// The native application instance handle. + /// Receives true if shutdown was requested. + /// A status code indicating success or failure. + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Application_IsShutdownRequested", SetLastError = true)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus ApplicationIsShutdownRequested(IntPtr instance, out byte value); + + /// + /// Registers the Win32 window class and sets DPI awareness. Windows only. + /// + /// The native application instance handle. + /// The Win32 HINSTANCE handle. + /// A status code indicating success or failure. + [SupportedOSPlatform("windows")] + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Application_register_win32", SetLastError = true)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus ApplicationRegisterWin32(IntPtr instance, IntPtr hInstance); + + /// + /// Sets up NSApplication delegate and activation policy. macOS only. + /// + /// The native application instance handle. + /// A status code indicating success or failure. + [SupportedOSPlatform("macos")] + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Application_register_mac", SetLastError = true)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus ApplicationRegisterMac(IntPtr instance); +} diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs index d1176d5c3..822bb3407 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs @@ -74,13 +74,4 @@ out IntPtr value [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_SetTeardownCallback", SetLastError = true)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial InfiniFrameNativeInteropStatus SetTeardownCallback(IntPtr instance, ContextAction callback, IntPtr context); - - /// - /// Shuts down the native UI thread and releases all global resources. - /// Must be called before process exit on Linux to prevent GLib assertion failures. - /// - /// A status code indicating success or failure. - [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Shutdown")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial InfiniFrameNativeInteropStatus Shutdown(); } diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.MacOs.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.MacOs.cs index a04dfb0a4..8b5b477db 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.MacOs.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.MacOs.cs @@ -10,15 +10,6 @@ namespace InfiniFrame.NativeBridge; // Code // --------------------------------------------------------------------------------------------------------------------- public partial class InfiniFrameNative { - /// - /// Registers the application with the macOS process (macOS only). - /// - /// A status code indicating success or failure. - [SupportedOSPlatform("macOS")] - [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_register_mac", SetLastError = true)] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial InfiniFrameNativeInteropStatus RegisterMac(); - /// /// Gets the native NSWindow handle for the specified instance (macOS only). /// diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.Windows.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.Windows.cs index e9fef64c8..c6fb3351b 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.Windows.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Platform.Windows.cs @@ -10,16 +10,6 @@ namespace InfiniFrame.NativeBridge; // Code // --------------------------------------------------------------------------------------------------------------------- public partial class InfiniFrameNative { - /// - /// Registers the Win32 window class (Windows only). - /// - /// The HINSTANCE for the application. - /// A status code indicating success or failure. - [SupportedOSPlatform("windows")] - [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_register_win32", SetLastError = true)] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial InfiniFrameNativeInteropStatus RegisterWin32(IntPtr hInstance); - /// /// Gets the native HWND handle for the specified instance (Windows only). /// diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/ApplicationInitParameters.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/ApplicationInitParameters.cs new file mode 100644 index 000000000..edd004599 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/ApplicationInitParameters.cs @@ -0,0 +1,48 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using System.Runtime.InteropServices; + +namespace InfiniFrame.NativeBridge.Parameters; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Represents the parameters used to configure and initialize a native InfiniFrame application. +/// Passed to the native layer as a sequentially laid-out struct. +/// +[StructLayout(LayoutKind.Sequential)] +public struct ApplicationInitParameters { + /// + /// The size of this struct. Used for ABI version checking. + /// + [MarshalAs(UnmanagedType.I4)] + public int StructSize; + + // ── Process identity (Win32) ────────────────────────────────────────── + + /// + /// WINDOWS ONLY: OPTIONAL: Explicit application identity used by the taskbar for grouping and pinning. + /// + public IntPtr WindowsAppUserModelId; + + /// + /// WINDOWS ONLY: OPTIONAL: Registers the application for toast notifications. + /// + public IntPtr NotificationRegistrationId; + + // ── WebView2 runtime path override (Win32) ──────────────────────────── + + /// + /// WINDOWS ONLY: OPTIONAL: Path to an extracted fixed-version WebView2 runtime. + /// + public IntPtr WebView2RuntimePath; + + // ── ABI version (must remain last) ──────────────────────────────────── + + /// + /// Reserved for future use. + /// + [MarshalAs(UnmanagedType.I4)] + public int Reserved; +} diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs index 2c20201e5..131d853e1 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs @@ -70,20 +70,6 @@ public struct InfiniFrameNativeParameters() { [MarshalAs(UnmanagedType.LPUTF8Str)] internal string? BrowserControlInitParameters; - /// - /// WINDOWS ONLY: OPTIONAL: Path to an extracted fixed-version WebView2 runtime used when the window is created. - /// - [MarshalAs(UnmanagedType.LPUTF8Str)] - internal string? WebView2RuntimePath; - - ///WINDOWS: OPTIONAL: Registers the application for toast notifications. If not provided, use Window Title. - [MarshalAs(UnmanagedType.LPUTF8Str)] - internal string? NotificationRegistrationId; - - ///WINDOWS: OPTIONAL: Explicit application identity used by the taskbar for grouping and pinning. - [MarshalAs(UnmanagedType.LPUTF8Str)] - internal string? WindowsAppUserModelId; - /// /// OPTIONAL: Default icon path applied to notifications when IconPath is not specified. /// @@ -393,6 +379,14 @@ public struct InfiniFrameNativeParameters() { [MarshalAs(UnmanagedType.LPUTF8Str)] internal string? MenuBarJson; + // Application handle (new in v2) + + /// + /// OPTIONAL: Pointer to the native InfiniFrameApplication instance. When provided, the window + /// uses the application's platform registration and message loop instead of managing its own. + /// + internal IntPtr ApplicationHandle; + // ABI version (must remain last) /// diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs index a2efd00cb..f56adaf47 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs @@ -51,9 +51,6 @@ public bool Equals(InfiniFrameNativeParameters x, InfiniFrameNativeParameters y) if (x.TemporaryFilesPath != y.TemporaryFilesPath) return false; if (x.UserAgent != y.UserAgent) return false; if (x.BrowserControlInitParameters != y.BrowserControlInitParameters) return false; - if (x.WebView2RuntimePath != y.WebView2RuntimePath) return false; - if (x.NotificationRegistrationId != y.NotificationRegistrationId) return false; - if (x.WindowsAppUserModelId != y.WindowsAppUserModelId) return false; if (x.DefaultNotificationIcon != y.DefaultNotificationIcon) return false; // Runtime configuration @@ -144,9 +141,6 @@ public int GetHashCode(InfiniFrameNativeParameters obj) { hashCode.Add(obj.TemporaryFilesPath); hashCode.Add(obj.UserAgent); hashCode.Add(obj.BrowserControlInitParameters); - hashCode.Add(obj.WebView2RuntimePath); - hashCode.Add(obj.NotificationRegistrationId); - hashCode.Add(obj.WindowsAppUserModelId); hashCode.Add(obj.DefaultNotificationIcon); // Runtime configuration diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs index 07bd05afe..90cfeae7f 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs @@ -86,9 +86,6 @@ internal struct Unmanaged { internal IntPtr TemporaryFilesPath; internal IntPtr UserAgent; internal IntPtr BrowserControlInitParameters; - internal IntPtr WebView2RuntimePath; - internal IntPtr NotificationRegistrationId; - internal IntPtr WindowsAppUserModelId; internal IntPtr DefaultNotificationIcon; // Runtime configuration @@ -181,6 +178,9 @@ internal struct Unmanaged { // Menu internal IntPtr MenuBarJson; + // Application handle (new in v2) + internal IntPtr ApplicationHandle; + // ABI version internal int Size; } @@ -240,9 +240,6 @@ public void FromManaged(InfiniFrameNativeParameters managed) { TemporaryFilesPath = ToUtf8Ptr(managed.TemporaryFilesPath), UserAgent = ToUtf8Ptr(managed.UserAgent), BrowserControlInitParameters = ToUtf8Ptr(managed.BrowserControlInitParameters), - WebView2RuntimePath = ToUtf8Ptr(managed.WebView2RuntimePath), - NotificationRegistrationId = ToUtf8Ptr(managed.NotificationRegistrationId), - WindowsAppUserModelId = ToUtf8Ptr(managed.WindowsAppUserModelId), DefaultNotificationIcon = ToUtf8Ptr(managed.DefaultNotificationIcon), // Runtime configuration @@ -335,6 +332,9 @@ public void FromManaged(InfiniFrameNativeParameters managed) { // Menu MenuBarJson = ToUtf8Ptr(managed.MenuBarJson), + // Application handle + ApplicationHandle = managed.ApplicationHandle, + // ABI version Size = managed.Size }; @@ -359,9 +359,6 @@ public void Free() { Marshal.FreeCoTaskMem(_unmanaged.TemporaryFilesPath); Marshal.FreeCoTaskMem(_unmanaged.UserAgent); Marshal.FreeCoTaskMem(_unmanaged.BrowserControlInitParameters); - Marshal.FreeCoTaskMem(_unmanaged.WebView2RuntimePath); - Marshal.FreeCoTaskMem(_unmanaged.NotificationRegistrationId); - Marshal.FreeCoTaskMem(_unmanaged.WindowsAppUserModelId); Marshal.FreeCoTaskMem(_unmanaged.DefaultNotificationIcon); Marshal.FreeCoTaskMem(_unmanaged.MenuBarJson); diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index 5eeacce35..ef162e555 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -87,12 +87,7 @@ public InfiniFrameNativeParametersValidator() { .NotNull().WithMessage("CustomSchemeNames must be specified.") .Must(names => names.Length <= 16).WithMessage("CustomSchemeNames must contain at most 16 names."); - RuleFor(p => p.WindowsAppUserModelId) - .NotEmpty() - .MaximumLength(128) - .Must(value => value is null || !value.Any(char.IsWhiteSpace)) - .When(p => p.WindowsAppUserModelId is not null) - .WithMessage("WindowsAppUserModelId must contain 1 to 128 characters and cannot contain whitespace."); + } // ----------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt b/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt index 6ab5b1c82..3b5e68889 100644 --- a/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt +++ b/src/InfiniFrame.NativeBridge/Native/CMakeLists.txt @@ -61,6 +61,7 @@ set(COMMON_SOURCES src/Api/Exports/Exports.Window.Setters.cpp src/Api/Exports/Exports.Window.Taskbar.cpp src/Api/Exports/Exports.Menu.cpp + src/Api/Exports/Exports.Application.cpp ) set(TEST_SOURCES @@ -84,6 +85,8 @@ set(WINDOWS_SOURCES src/Runtime/Platform/Windows/Core/WindowState.Win32.cpp src/Runtime/Platform/Windows/Core/WindowStorage.Win32.cpp src/Runtime/Platform/Windows/Core/WindowTracing.Win32.cpp + src/Runtime/Platform/Windows/Core/ApplicationCore.Win32.cpp + src/Runtime/Platform/Windows/Core/ApplicationLifecycle.Win32.cpp src/Runtime/Platform/Windows/DarkMode.cpp src/Runtime/Platform/Windows/Dialog.cpp src/Runtime/Platform/Windows/Dpi.Win32.cpp @@ -114,6 +117,8 @@ set(LINUX_SOURCES src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp src/Runtime/Platform/Linux/Core/WindowSignals.Gtk.cpp src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp + src/Runtime/Platform/Linux/Core/ApplicationCore.Gtk.cpp + src/Runtime/Platform/Linux/Core/ApplicationLifecycle.Gtk.cpp src/Runtime/Platform/Linux/Dialog.cpp src/Runtime/Platform/Linux/Menu.Gtk.cpp src/Runtime/Platform/Linux/Monitors.Gtk.cpp @@ -143,6 +148,8 @@ set(MAC_SOURCES src/Runtime/Platform/Mac/Core/WindowEvents.Cocoa.mm src/Runtime/Platform/Mac/Core/WindowLifecycle.Cocoa.mm src/Runtime/Platform/Mac/Core/WindowState.Cocoa.mm + src/Runtime/Platform/Mac/Core/ApplicationCore.Cocoa.mm + src/Runtime/Platform/Mac/Core/ApplicationLifecycle.Cocoa.mm src/Runtime/Platform/Mac/Delegates/AppDelegate.mm src/Runtime/Platform/Mac/Delegates/NavigationDelegate.mm src/Runtime/Platform/Mac/Delegates/UiDelegate.mm @@ -176,6 +183,9 @@ set(HEADER_FILES src/Runtime/Shared/Window/InfiniFrameDialog.h src/Runtime/Shared/Window/InfiniFrameInitParams.h src/Runtime/Shared/Window/InfiniFrameWindow.h + src/Runtime/Shared/Application/InfiniFrameApplication.h + src/Runtime/Shared/Application/InfiniFrameApplicationImpl.h + src/Runtime/Shared/Application/ApplicationInitParams.h src/Runtime/Shared/Types/Basic.h src/Runtime/Shared/Types/Callbacks.h src/Runtime/Shared/Types/DialogButtons.h diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Application.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Application.cpp new file mode 100644 index 000000000..4c2459302 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Application.cpp @@ -0,0 +1,117 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Api/Exports/Exports.h" +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/ApplicationInitParams.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +extern "C" { + +/// @brief Creates a new application instance with the given parameters. +/// @param params Application initialization parameters. +/// @param[out] value Receives the newly created application handle. +/// @return InteropStatus +EXPORTED InteropStatus InfiniFrameNative_Application_ctor( + ApplicationInitParams* params, InfiniFrameApplication** value) { + ResetOut(value, static_cast(nullptr)); + return RunExportStatus( + [&] { + if (!EnsureOutNotNull(value, "value")) + return; + if (params == nullptr) + throw std::invalid_argument("Argument 'params' is null."); + if (params->StructSize != static_cast(sizeof(ApplicationInitParams))) + throw std::invalid_argument("ApplicationInitParams size mismatch."); + auto instance = std::make_unique(params); + *value = instance.release(); + }); +} + +/// @brief Destroys the application instance and releases resources. +/// @param instance The application handle to destroy. +/// @return InteropStatus +EXPORTED InteropStatus InfiniFrameNative_Application_dtor(InfiniFrameApplication* instance) { + return RunExportStatus( + [&] { + if (!EnsureNotNull(instance, "instance")) + return; + std::unique_ptr guard{instance}; + }); +} + +/// @brief Runs the application message loop, blocking until all windows close or Shutdown() is called. +/// @param instance The application handle. +/// @return InteropStatus +EXPORTED InteropStatus InfiniFrameNative_Application_Run(InfiniFrameApplication* instance) { + return RunExportStatus( + [&] { + if (!EnsureNotNull(instance, "instance")) + return; + instance->Run(); + }); +} + +/// @brief Signals the application message loop to exit. +/// @param instance The application handle. +/// @return InteropStatus +EXPORTED InteropStatus InfiniFrameNative_Application_Shutdown(InfiniFrameApplication* instance) { + return RunExportStatus( + [&] { + if (!EnsureNotNull(instance, "instance")) + return; + instance->Shutdown(); + }); +} + +/// @brief Checks if Shutdown() has been called. +/// @param instance The application handle. +/// @param[out] value Receives true if shutdown was requested. +/// @return InteropStatus +EXPORTED InteropStatus InfiniFrameNative_Application_IsShutdownRequested( + InfiniFrameApplication* instance, bool* value) { + ResetOut(value, false); + return RunExportStatus( + [&] { + if (!EnsureNotNull(instance, "instance")) + return; + if (!EnsureOutNotNull(value, "value")) + return; + *value = instance->IsShutdownRequested(); + }); +} + +#ifdef _WIN32 +/// @brief Registers the Win32 window class and sets DPI awareness. +/// @param instance The application handle. +/// @param hInstance The application instance handle. +/// @return InteropStatus +EXPORTED InteropStatus InfiniFrameNative_Application_register_win32( + InfiniFrameApplication* instance, HINSTANCE hInstance) { + return RunExportStatus( + [&] { + if (!EnsureNotNull(instance, "instance")) + return; + if (hInstance == nullptr) + throw std::invalid_argument("Argument 'hInstance' is null."); + instance->Register(hInstance); + }); +} +#endif + +#ifdef __APPLE__ +/// @brief Sets up NSApplication delegate and activation policy. +/// @param instance The application handle. +/// @return InteropStatus +EXPORTED InteropStatus InfiniFrameNative_Application_register_mac(InfiniFrameApplication* instance) { + return RunExportStatus( + [&] { + if (!EnsureNotNull(instance, "instance")) + return; + instance->Register(); + }); +} +#endif + +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp index 73b6c49e6..8ec445d09 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp @@ -2,8 +2,8 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- #include "Api/Exports/Exports.h" -#ifdef __linux__ -#include "Runtime/Platform/Linux/Core/UiThread.Gtk.h" +#ifdef __APPLE__ +#include "Runtime/Platform/Mac/MacDiagnostics.h" #endif // --------------------------------------------------------------------------------------------------------------------- // Code @@ -14,6 +14,9 @@ extern "C" { /// @param[out] value Receives the newly created window handle. /// @return InteropStatus EXPORTED InteropStatus InfiniFrameNative_ctor(InfiniFrameInitParams* initParams, InfiniFrameWindow** value) { +#ifdef __APPLE__ + infiniframe::macos::InstallDiagnostics(); +#endif ResetOut(value, static_cast(nullptr)); return RunExportStatus( [&] { @@ -103,15 +106,4 @@ EXPORTED InteropStatus InfiniFrameNative_SetTeardownCallback( window->SetTeardownCallback(callback, context); }); } - -#ifdef __linux__ -/// @brief Forces immediate shutdown of the native window (Linux only). -/// @return InteropStatus -EXPORTED InteropStatus InfiniFrameNative_Shutdown() { - return RunExportStatus( - [] { - infiniframe::linux_gtk::ui_thread::Shutdown(); - }); -} -#endif } \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.MacOs.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.MacOs.cpp index 885b695fe..4a1345891 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.MacOs.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.MacOs.cpp @@ -7,15 +7,6 @@ // --------------------------------------------------------------------------------------------------------------------- extern "C" { #ifdef __APPLE__ -/// @brief Registers the macOS window class. -/// @return InteropStatus -EXPORTED InteropStatus InfiniFrameNative_register_mac() { - return RunExportStatus( - [] { - InfiniFrameWindow::Register(); - }); -} - /// @brief Gets the NSWindow handle for the window. /// @param instance The window handle. /// @param[out] value Receives the NSWindow pointer as void*. diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.Windows.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.Windows.cpp index b0dfc3b6c..730666c86 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.Windows.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Platform.Windows.cpp @@ -10,18 +10,6 @@ // --------------------------------------------------------------------------------------------------------------------- extern "C" { #ifdef _WIN32 -/// @brief Registers the Win32 window class. -/// @param hInstance The application instance handle. -/// @return InteropStatus -EXPORTED InteropStatus InfiniFrameNative_register_win32(const HINSTANCE hInstance) { - return RunExportStatus( - [&] { - if (hInstance == nullptr) - throw std::invalid_argument("Argument 'hInstance' is null."); - InfiniFrameWindow::Register(hInstance); - }); -} - /// @brief Gets the Win32 HWND handle for the window. /// @param instance The window handle. /// @param[out] value Receives the HWND handle. diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Testing/Exports.Tests.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Testing/Exports.Tests.cpp index 064d30c9f..4c943d42b 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Testing/Exports.Tests.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Testing/Exports.Tests.cpp @@ -54,9 +54,6 @@ EXPORTED InteropStatus InfiniFrameNativeTests_NativeParametersReturnAsIs( (*new_params)->TemporaryFilesPath = DuplicateString(params->TemporaryFilesPath); (*new_params)->UserAgent = DuplicateString(params->UserAgent); (*new_params)->BrowserControlInitParameters = DuplicateString(params->BrowserControlInitParameters); - (*new_params)->WebView2RuntimePath = DuplicateString(params->WebView2RuntimePath); - (*new_params)->NotificationRegistrationId = DuplicateString(params->NotificationRegistrationId); - (*new_params)->WindowsAppUserModelId = DuplicateString(params->WindowsAppUserModelId); (*new_params)->DefaultNotificationIcon = DuplicateString(params->DefaultNotificationIcon); // Runtime configuration @@ -158,9 +155,6 @@ EXPORTED InteropStatus InfiniFrameNativeTests_FreeInitParams(InfiniFrameInitPara delete[] params->TemporaryFilesPath; delete[] params->UserAgent; delete[] params->BrowserControlInitParameters; - delete[] params->WebView2RuntimePath; - delete[] params->NotificationRegistrationId; - delete[] params->WindowsAppUserModelId; delete[] params->DefaultNotificationIcon; for (size_t i = 0; i < InfiniFrameInitParams::MaxCustomSchemeNames; ++i) { delete[] params->CustomSchemeNames[i]; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/ApplicationCore.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/ApplicationCore.Gtk.cpp new file mode 100644 index 000000000..dfb53d037 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/ApplicationCore.Gtk.cpp @@ -0,0 +1,58 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/InfiniFrameApplicationImpl.h" +#include "Runtime/Shared/Application/ApplicationInitParams.h" +#include "Runtime/Platform/Linux/Core/UiThread.Gtk.h" +#include +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +InfiniFrameApplication* InfiniFrameApplication::s_instance = nullptr; + +InfiniFrameApplication* InfiniFrameApplication::GetInstance() { + return s_instance; +} + +InfiniFrameApplication::InfiniFrameApplication(ApplicationInitParams* params) { + m_impl = std::make_unique(); + s_instance = this; + + if (params == nullptr) + throw std::invalid_argument("Argument 'params' is null."); + + if (params->StructSize != sizeof(ApplicationInitParams)) + throw std::invalid_argument("ApplicationInitParams size mismatch."); + + infiniframe::linux_gtk::ui_thread::EnsureInitialized(); +} + +InfiniFrameApplication::~InfiniFrameApplication() { + s_instance = nullptr; + infiniframe::linux_gtk::ui_thread::Shutdown(); +} + +void InfiniFrameApplication::TrackWindow(InfiniFrameWindow* window) { + std::lock_guard lock(m_impl->_windowListMutex); + m_impl->_windows.push_back(window); +} + +void InfiniFrameApplication::UntrackWindow(InfiniFrameWindow* window) { + std::lock_guard lock(m_impl->_windowListMutex); + auto it = std::remove(m_impl->_windows.begin(), m_impl->_windows.end(), window); + m_impl->_windows.erase(it, m_impl->_windows.end()); + + if (m_impl->_windows.empty() && !m_impl->_shutdownRequested.load(std::memory_order_acquire)) { + Shutdown(); + } +} + +bool InfiniFrameApplication::HasWindows() const { + std::lock_guard lock(m_impl->_windowListMutex); + return !m_impl->_windows.empty(); +} + +bool InfiniFrameApplication::IsShutdownRequested() const { + return m_impl->_shutdownRequested.load(std::memory_order_acquire); +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/ApplicationLifecycle.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/ApplicationLifecycle.Gtk.cpp new file mode 100644 index 000000000..05d87a338 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/ApplicationLifecycle.Gtk.cpp @@ -0,0 +1,22 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/InfiniFrameApplicationImpl.h" +#include "Runtime/Platform/Linux/Core/UiThread.Gtk.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameApplication::Run() { + gtk_main(); +} + +void InfiniFrameApplication::Shutdown() { + bool expected = false; + if (!m_impl->_shutdownRequested.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) + return; + + infiniframe::linux_gtk::ui_thread::InvokeSync([] { + gtk_main_quit(); + }); +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowCore.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowCore.Gtk.cpp index 0008f2abf..a13065a59 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowCore.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowCore.Gtk.cpp @@ -6,13 +6,12 @@ #include "Runtime/Platform/Linux/Core/UiThread.Gtk.h" #include "Runtime/Platform/Linux/Window.Gtk.Internal.h" +#include "Runtime/Shared/Application/InfiniFrameApplication.h" // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : m_impl(std::make_unique()) { - infiniframe::linux_gtk::ui_thread::EnsureInitialized(); - if (initParams->StructSize != sizeof(InfiniFrameInitParams)) { throw std::invalid_argument( "Initial parameters passed are " + std::to_string(initParams->StructSize) + @@ -20,6 +19,11 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : ); } + if (initParams->ApplicationHandle == nullptr) + throw std::invalid_argument("ApplicationHandle is required. Create an InfiniFrameApplication first."); + m_impl->_application = static_cast(initParams->ApplicationHandle); + m_impl->_application->TrackWindow(this); + infiniframe::linux_gtk::ui_thread::InvokeSync( [this, initParams] { m_impl->InitializeFromParams(initParams); @@ -49,6 +53,8 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : } InfiniFrameWindow::~InfiniFrameWindow() { + m_impl->_application->UntrackWindow(this); + infiniframe::linux_gtk::ui_thread::InvokeSync( [this] { if (m_impl->_window != nullptr) { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp index bd03ba32c..b1296e1f4 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp @@ -58,13 +58,16 @@ namespace { continue; int64_t type = 0; - (void)obj["type"].get_int64().get(type); + if (obj["type"].get_int64().get(type) != simdjson::SUCCESS) + type = 0; bool isEnabled = true; - (void)obj["isEnabled"].get_bool().get(isEnabled); + if (obj["isEnabled"].get_bool().get(isEnabled) != simdjson::SUCCESS) + isEnabled = true; bool isVisible = true; - (void)obj["isVisible"].get_bool().get(isVisible); + if (obj["isVisible"].get_bool().get(isVisible) != simdjson::SUCCESS) + isVisible = true; if (!isVisible) continue; @@ -76,7 +79,8 @@ namespace { } std::string label; - (void)obj["label"].get_string().get(label); + if (obj["label"].get_string().get(label) != simdjson::SUCCESS) + label.clear(); guint commandId = nextId++; idToCommand[id] = commandId; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/ApplicationCore.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/ApplicationCore.Cocoa.mm new file mode 100644 index 000000000..36ee216d6 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/ApplicationCore.Cocoa.mm @@ -0,0 +1,79 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#import +#include + +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/InfiniFrameApplicationImpl.h" +#include "Runtime/Shared/Application/ApplicationInitParams.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +@interface InfiniFrameAppDelegate : NSObject +@end + +@implementation InfiniFrameAppDelegate +- (void)applicationDidFinishLaunching:(NSNotification *)notification { + (void)notification; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { + (void)sender; + return YES; +} +@end + +InfiniFrameApplication* InfiniFrameApplication::s_instance = nullptr; + +InfiniFrameApplication* InfiniFrameApplication::GetInstance() { + return s_instance; +} + +InfiniFrameApplication::InfiniFrameApplication(ApplicationInitParams* params) { + m_impl = std::make_unique(); + s_instance = this; + + if (params == nullptr) + throw std::invalid_argument("Argument 'params' is null."); + + if (params->StructSize != sizeof(ApplicationInitParams)) + throw std::invalid_argument("ApplicationInitParams size mismatch."); +} + +InfiniFrameApplication::~InfiniFrameApplication() { + s_instance = nullptr; +} + +void InfiniFrameApplication::Register() { + @autoreleasepool { + InfiniFrameAppDelegate* delegate = [[InfiniFrameAppDelegate alloc] init]; + [NSApp setDelegate:delegate]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp activateIgnoringOtherApps:YES]; + } +} + +void InfiniFrameApplication::TrackWindow(InfiniFrameWindow* window) { + std::lock_guard lock(m_impl->_windowListMutex); + m_impl->_windows.push_back(window); +} + +void InfiniFrameApplication::UntrackWindow(InfiniFrameWindow* window) { + std::lock_guard lock(m_impl->_windowListMutex); + auto it = std::remove(m_impl->_windows.begin(), m_impl->_windows.end(), window); + m_impl->_windows.erase(it, m_impl->_windows.end()); + + if (m_impl->_windows.empty() && !m_impl->_shutdownRequested.load(std::memory_order_acquire)) { + Shutdown(); + } +} + +bool InfiniFrameApplication::HasWindows() const { + std::lock_guard lock(m_impl->_windowListMutex); + return !m_impl->_windows.empty(); +} + +bool InfiniFrameApplication::IsShutdownRequested() const { + return m_impl->_shutdownRequested.load(std::memory_order_acquire); +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/ApplicationLifecycle.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/ApplicationLifecycle.Cocoa.mm new file mode 100644 index 000000000..2f8c16c07 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/ApplicationLifecycle.Cocoa.mm @@ -0,0 +1,22 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#import +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/InfiniFrameApplicationImpl.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameApplication::Run() { + [NSApp run]; +} + +void InfiniFrameApplication::Shutdown() { + bool expected = false; + if (!m_impl->_shutdownRequested.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) + return; + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSApp terminate:nil]; + }); +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/UiDispatcher.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/UiDispatcher.Cocoa.mm index e8db823dc..ae66b8efc 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/UiDispatcher.Cocoa.mm +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/UiDispatcher.Cocoa.mm @@ -51,19 +51,18 @@ } bool InfiniFrameWindow::ScheduleOperation(const std::shared_ptr& operation) { - CFRunLoopRef mainRunLoop = CFRunLoopGetMain(); - if (mainRunLoop == nullptr) - return false; - - // `operation` is a reference parameter. Capturing it directly in an Objective-C block - // retains the reference variable rather than creating a new shared_ptr owner. The - // operation map may release its owner while this block is still queued (for example, - // during window teardown), leaving the run-loop callback with a dangling reference. - // Materialize an owning local copy before the block is formed. + // Use dispatch_async instead of CFRunLoopPerformBlock. On macOS, GCD dispatch + // sources (used by ScheduleDeferredDestruction β†’ dispatch_async(delete this)) + // are drained BEFORE CFRunLoopPerformBlock blocks in the same run-loop + // iteration. This caused a race: delete this could destroy m_impl before a + // pending Execute() block ran, causing use-after-free on the operation mutex. + // + // By using dispatch_async, both operation execution and delete this are on the + // same serial main queue. FIFO ordering guarantees that an operation enqueued + // before delete this will execute first, while m_impl is still alive. const std::shared_ptr retainedOperation = operation; - CFRunLoopPerformBlock(mainRunLoop, kCFRunLoopCommonModes, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ retainedOperation->Execute(); }); - CFRunLoopWakeUp(mainRunLoop); return true; } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowCore.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowCore.Cocoa.mm index 79569750a..8cb99ddaf 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowCore.Cocoa.mm +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowCore.Cocoa.mm @@ -27,6 +27,7 @@ #include "../MacDiagnostics.h" #include "../Window.Cocoa.Internal.h" #include "../Delegates/WindowDelegate.h" +#include "Runtime/Shared/Application/InfiniFrameApplication.h" // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- @@ -139,83 +140,15 @@ size_t PooledMacHostCountForTesting() { else DestroyMacHost(host); } -void InfiniFrameWindow::Register() -{ - infiniframe::macos::InstallDiagnostics(); - infiniframe::macos::LogLifecycle("register", nullptr); - DispatchToMainSync(^{ - static dispatch_once_t registerOnceToken; - dispatch_once(®isterOnceToken, ^{ - @autoreleasepool { - NSApplication *application = [NSApplication sharedApplication]; - // NSApplication's delegate is not an ownership boundary on every supported - // SDK. Keep our delegate alive for the process lifetime, and do not replace an - // embedding application's delegate. - static AppDelegate *appDelegate = [[AppDelegate alloc] init]; - if ([application delegate] == nil) - [application setDelegate: appDelegate]; - [application setActivationPolicy: NSApplicationActivationPolicyRegular]; - - NSString *appName = [[NSProcessInfo processInfo] processName]; - - NSMenu *mainMenu = [[NSMenu new] autorelease]; - NSMenuItem *mainMenuItem = [[NSMenuItem new] autorelease]; - [mainMenu addItem: mainMenuItem]; - - NSMenu *mainSubMenu = [[NSMenu new] autorelease]; - [mainMenuItem setSubmenu: mainSubMenu]; - - NSMenuItem *selectMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Select All" - action: @selector(selectAll:) - keyEquivalent: @"a" - ] autorelease]; - [mainSubMenu addItem: selectMenuItem]; - - NSMenuItem *cutMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Cut" - action: @selector(cut:) - keyEquivalent: @"x" - ] autorelease]; - [mainSubMenu addItem: cutMenuItem]; - - NSMenuItem *copyMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Copy" - action: @selector(copy:) - keyEquivalent: @"c" - ] autorelease]; - [mainSubMenu addItem: copyMenuItem]; - - NSMenuItem *pasteMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: @"Paste" - action: @selector(paste:) - keyEquivalent: @"v" - ] autorelease]; - [mainSubMenu addItem: pasteMenuItem]; - - NSMenuItem *quitMenuItem = [[ - [NSMenuItem alloc] - initWithTitle: [@"Quit " stringByAppendingString: appName] - action: @selector(terminate:) - keyEquivalent: @"q" - ] autorelease]; - [mainSubMenu addItem: quitMenuItem]; - - [NSApp setMainMenu: mainMenu]; - if (![application isRunning]) - [application finishLaunching]; - } - }); - }); -} - InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) : m_impl(std::make_unique()) { infiniframe::macos::LogLifecycle("window-construct-begin", this); + + if (initParams->ApplicationHandle == nullptr) + throw std::invalid_argument("ApplicationHandle is required. Create an InfiniFrameApplication first."); + m_impl->_application = static_cast(initParams->ApplicationHandle); + m_impl->_application->TrackWindow(this); + const bool traceTimings = std::getenv("INFINIFRAME_MACOS_TRACE_TIMINGS") != nullptr; const auto constructionStartedAt = std::chrono::steady_clock::now(); __block std::chrono::steady_clock::time_point webViewStartedAt; @@ -591,6 +524,10 @@ size_t PooledMacHostCountForTesting() { InfiniFrameWindow::~InfiniFrameWindow() { infiniframe::macos::LogLifecycle("window-destruct-begin", this); + _destroying.store(true, std::memory_order_release); + + m_impl->_application->UntrackWindow(this); + // SafeHandle finalization and managed disposal can release the native window from a // non-AppKit thread. All Cocoa/WebKit teardown must therefore occur on the main queue. DispatchToMainSync(^{ diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowLifecycle.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowLifecycle.Cocoa.mm index 618cf9ade..9d074665d 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowLifecycle.Cocoa.mm +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowLifecycle.Cocoa.mm @@ -122,19 +122,13 @@ static void DispatchToMainSync(void (^block)()) { void InfiniFrameWindow::CompleteCloseAfterWebKitTeardown() { infiniframe::macos::LogLifecycle("window-webkit-teardown-complete", this); - { - infiniframe::macos::NativeCallbackScope callbackScope; - InvokeClosed(); - } + SignalWindowClosed(); ScheduleTeardownCompletion(); - // Defer one main-queue turn so SafeHandle disposal from a reverse P/Invoke callback never - // deletes the C++ session while AppKit is unwinding through that callback. - if (m_impl->_nativeDestructionScheduled) { - dispatch_async(dispatch_get_main_queue(), ^{ - delete this; - }); + { + infiniframe::macos::NativeCallbackScope callbackScope; + InvokeClosed(); } } @@ -153,11 +147,12 @@ static void DispatchToMainSync(void (^block)()) { void InfiniFrameWindow::ScheduleDeferredDestruction() { void (^requestDestruction)() = ^{ - if (this->m_impl->_nativeDestructionScheduled) + bool expected = false; + if (!this->m_impl->_nativeDestructionScheduled.compare_exchange_strong(expected, true, + std::memory_order_acq_rel, std::memory_order_relaxed)) return; infiniframe::macos::LogLifecycle("window-destruction-request", this); - this->m_impl->_nativeDestructionScheduled = true; if (!this->m_impl->_isClosingOrClosed) { this->CloseWebView(); this->PrepareForDeferredDestruction(); @@ -168,9 +163,13 @@ static void DispatchToMainSync(void (^block)()) { // Still defer one main-queue turn so disposal from // an AppKit delegate cannot delete the instance while that delegate is executing. - dispatch_async(dispatch_get_main_queue(), ^{ - delete this; - }); + bool deletionExpected = false; + if (this->m_impl->_deletionQueued.compare_exchange_strong(deletionExpected, true, + std::memory_order_acq_rel, std::memory_order_relaxed)) { + dispatch_async(dispatch_get_main_queue(), ^{ + delete this; + }); + } }; if ([NSThread isMainThread]) { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Window.Cocoa.Internal.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Window.Cocoa.Internal.h index 940b3e00f..0dba2df33 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Window.Cocoa.Internal.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Window.Cocoa.Internal.h @@ -60,7 +60,8 @@ struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { bool _chromeless = false; bool _webviewReady = false; bool _isClosingOrClosed = false; - bool _nativeDestructionScheduled = false; + std::atomic _nativeDestructionScheduled{false}; + std::atomic _deletionQueued{false}; std::string _hostCompatibilityKey; std::atomic _windowClosed = false; std::mutex _windowClosedMutex; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/ApplicationCore.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/ApplicationCore.Win32.cpp new file mode 100644 index 000000000..d558437fe --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/ApplicationCore.Win32.cpp @@ -0,0 +1,136 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +#include + +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/InfiniFrameApplicationImpl.h" +#include "Runtime/Shared/Application/ApplicationInitParams.h" +#include "Runtime/Platform/Windows/DarkMode.h" +#include "Runtime/Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +using namespace WinToastLib; + +InfiniFrameApplication* InfiniFrameApplication::s_instance = nullptr; + +InfiniFrameApplication* InfiniFrameApplication::GetInstance() { + return s_instance; +} + +InfiniFrameApplication::InfiniFrameApplication(ApplicationInitParams* params) { + m_impl = std::make_unique(); + s_instance = this; + + if (params == nullptr) + throw std::invalid_argument("Argument 'params' is null."); + + if (params->StructSize != sizeof(ApplicationInitParams)) + throw std::invalid_argument("ApplicationInitParams size mismatch."); + + // Process-wide: AppUserModelId + if (params->WindowsAppUserModelId != nullptr && params->WindowsAppUserModelId[0] != '\0') { + m_impl->_appUserModelId = Utf8ToWide(params->WindowsAppUserModelId); + const HRESULT result = SetCurrentProcessExplicitAppUserModelID(m_impl->_appUserModelId.c_str()); + if (FAILED(result)) { + throw std::runtime_error( + std::format( + "Could not set Windows AppUserModelID (HRESULT 0x{:08X}).", + static_cast(result) + ) + ); + } + } + + // Process-wide: WinToast + WinToastLib::setDebugOutputEnabled(false); + + if (params->NotificationRegistrationId != nullptr) + m_impl->_notificationRegistrationId = Utf8ToWide(params->NotificationRegistrationId); + + // WebView2 runtime path + if (params->WebView2RuntimePath != nullptr) + m_impl->_webView2RuntimePath = Utf8ToWide(params->WebView2RuntimePath); +} + +InfiniFrameApplication::~InfiniFrameApplication() { + s_instance = nullptr; +} + +void InfiniFrameApplication::Register(const HINSTANCE hInstance) { + InitDarkModeSupport(); + + m_impl->_hInstance = hInstance; + m_impl->_messageLoopThreadId = GetCurrentThreadId(); + + WNDCLASSEX wcx{}; + wcx.cbSize = sizeof(WNDCLASSEX); + wcx.style = CS_HREDRAW | CS_VREDRAW; + wcx.lpfnWndProc = WindowProc; + wcx.hInstance = hInstance; + wcx.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wcx.hCursor = LoadCursor(nullptr, IDC_ARROW); + wcx.hbrBackground = IsDarkModeEnabled() ? GetDarkBrush() : GetLightBrush(); + wcx.lpszClassName = CLASS_NAME; + wcx.hIconSm = LoadIcon(hInstance, IDI_APPLICATION); + + if (RegisterClassEx(&wcx) == 0) { + const DWORD error = GetLastError(); + if (error != ERROR_CLASS_ALREADY_EXISTS) + throw std::runtime_error("RegisterClassEx failed for window class 'InfiniFrame'."); + } + + SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + // Initialize WinToast once at the application level. + if (!m_impl->_appUserModelId.empty()) + WinToast::instance()->setAppUserModelId(m_impl->_appUserModelId.c_str()); + else if (!m_impl->_notificationRegistrationId.empty()) + WinToast::instance()->setAppUserModelId(m_impl->_notificationRegistrationId.c_str()); + WinToast::instance()->initialize(); +} + +HINSTANCE InfiniFrameApplication::GetHInstance() const { + return m_impl->_hInstance; +} + +void InfiniFrameApplication::TrackWindow(InfiniFrameWindow* window) { + std::lock_guard lock(m_impl->_windowListMutex); + m_impl->_windows.push_back(window); +} + +void InfiniFrameApplication::UntrackWindow(InfiniFrameWindow* window) { + std::lock_guard lock(m_impl->_windowListMutex); + auto it = std::remove(m_impl->_windows.begin(), m_impl->_windows.end(), window); + m_impl->_windows.erase(it, m_impl->_windows.end()); + + if (m_impl->_windows.empty() && !m_impl->_shutdownRequested.load(std::memory_order_acquire)) { + Shutdown(); + } +} + +bool InfiniFrameApplication::HasWindows() const { + std::lock_guard lock(m_impl->_windowListMutex); + return !m_impl->_windows.empty(); +} + +bool InfiniFrameApplication::IsShutdownRequested() const { + return m_impl->_shutdownRequested.load(std::memory_order_acquire); +} + +const std::wstring& InfiniFrameApplication::GetAppUserModelId() const { + return m_impl->_appUserModelId; +} + +const std::wstring& InfiniFrameApplication::GetNotificationRegistrationId() const { + return m_impl->_notificationRegistrationId; +} + +const std::wstring& InfiniFrameApplication::GetWebView2RuntimePath() const { + return m_impl->_webView2RuntimePath; +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/ApplicationLifecycle.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/ApplicationLifecycle.Win32.cpp new file mode 100644 index 000000000..0a6ab6ef4 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/ApplicationLifecycle.Win32.cpp @@ -0,0 +1,37 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/InfiniFrameApplicationImpl.h" +#include "Runtime/Platform/Windows/Window.Win32.Context.h" +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +void InfiniFrameApplication::Run() { + m_impl->_messageLoopThreadId = GetCurrentThreadId(); + + MSG msg = {}; + while (!m_impl->_shutdownRequested.load(std::memory_order_acquire)) { + const int getMessageResult = GetMessage(&msg, nullptr, 0, 0); + if (getMessageResult == -1) + break; + if (getMessageResult == 0) + break; + + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +void InfiniFrameApplication::Shutdown() { + bool expected = false; + if (!m_impl->_shutdownRequested.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) + return; + + DWORD threadId = m_impl->_messageLoopThreadId; + if (threadId != 0 && threadId != GetCurrentThreadId()) { + PostThreadMessage(threadId, WM_QUIT, 0, 0); + } else { + PostQuitMessage(0); + } +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp index 4f1c18356..a9c43d395 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp @@ -8,14 +8,14 @@ #include "Runtime/Platform/Windows/DarkMode.h" #include "Runtime/Platform/Windows/Window.Win32.Context.h" +#include "Runtime/Shared/Application/InfiniFrameApplication.h" +#include "Runtime/Shared/Application/InfiniFrameApplicationImpl.h" // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- static_assert(sizeof(wchar_t) == sizeof(char16_t)); auto CLASS_NAME = L"InfiniFrame"; -std::atomic _hInstance{nullptr}; -thread_local HWND messageLoopRootWindowHandle = nullptr; using namespace WinToastLib; @@ -65,32 +65,6 @@ HBRUSH GetLightBrush() { return BrushManager::instance().light(); } -void InfiniFrameWindow::Register(const HINSTANCE hInstance) { - InitDarkModeSupport(); - - _hInstance.store(hInstance, std::memory_order_release); - - WNDCLASSEX wcx{}; - wcx.cbSize = sizeof(WNDCLASSEX); - wcx.style = CS_HREDRAW | CS_VREDRAW; - wcx.lpfnWndProc = WindowProc; - wcx.cbClsExtra = 0; - wcx.cbWndExtra = 0; - wcx.hInstance = hInstance; - wcx.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - wcx.hCursor = LoadCursor(nullptr, IDC_ARROW); - wcx.hbrBackground = IsDarkModeEnabled() ? GetDarkBrush() : GetLightBrush(); - wcx.lpszMenuName = nullptr; - wcx.lpszClassName = CLASS_NAME; - wcx.hIconSm = LoadIcon(hInstance, IDI_APPLICATION); - - if (RegisterClassEx(&wcx) == 0) { - throw std::runtime_error("RegisterClassEx failed for window class 'InfiniFrame'."); - } - - SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); -} - // Initializes native window lifecycle state from host-provided startup parameters. // Flow: // 1) Allocate implementation storage. @@ -101,10 +75,6 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { // Backing implementation object must exist before any field assignment. m_impl = std::make_unique(); - // WinToast writes verbose diagnostics directly to stdout in Debug builds. Test hosts transport stdout over RPC; - // hundreds of window lifecycle tests can otherwise flood and destabilize IDE test-runner connections. - WinToastLib::setDebugOutputEnabled(false); - // Fail fast if caller and native side disagree on struct layout/version. if (initParams->StructSize != sizeof(InfiniFrameInitParams)) { throw std::invalid_argument( @@ -113,19 +83,11 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { ); } - if (initParams->WindowsAppUserModelId != nullptr && initParams->WindowsAppUserModelId[0] != '\0') { - const std::wstring appUserModelId = ToUTF16String(initParams->WindowsAppUserModelId); - m_impl->_windowsAppUserModelId = appUserModelId; - const HRESULT result = SetCurrentProcessExplicitAppUserModelID(appUserModelId.c_str()); - if (FAILED(result)) { - throw std::runtime_error( - std::format( - "Could not set Windows AppUserModelID (HRESULT 0x{:08X}).", - static_cast(result) - ) - ); - } - } + // Store application reference β€” required for the window to function. + if (initParams->ApplicationHandle == nullptr) + throw std::invalid_argument("ApplicationHandle is required. Create an InfiniFrameApplication first."); + m_impl->_application = static_cast(initParams->ApplicationHandle); + m_impl->_application->TrackWindow(this); // Initialize window title and optional toast notification identity. if (initParams->Title != nullptr) { @@ -152,11 +114,9 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { if (initParams->BrowserControlInitParameters != nullptr) m_impl->_browserControlInitParameters = ToUTF16String(initParams->BrowserControlInitParameters); - if (initParams->WebView2RuntimePath != nullptr) - m_impl->_webView2RuntimePath = ToUTF16String(initParams->WebView2RuntimePath); + // Read application-level config from the application instance instead of initParams. + m_impl->_webView2RuntimePath = m_impl->_application->GetWebView2RuntimePath(); - if (initParams->NotificationRegistrationId != nullptr) - m_impl->_notificationRegistrationId = ToUTF16String(initParams->NotificationRegistrationId); m_impl->_remoteDebuggingPort = initParams->RemoteDebuggingPort; m_impl->_transparentEnabled = initParams->Transparent; @@ -262,7 +222,7 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { const HWND parentWindowHandle = ResolveParentWindowHandle(m_impl->_parent); m_impl->_pendingOwnerHwnd = parentWindowHandle; - const HINSTANCE windowInstance = _hInstance.load(std::memory_order_acquire); + const HINSTANCE windowInstance = m_impl->_application->GetHInstance(); m_impl->_hWnd = CreateWindowEx( initParams->Transparent ? WS_EX_LAYERED : 0, CLASS_NAME, m_impl->_windowTitle.c_str(), initParams->Chromeless || initParams->FullScreen ? WS_POPUP : WS_OVERLAPPEDWINDOW, normalizedLeft, @@ -294,15 +254,10 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { SetTopmost(true); if (initParams->NotificationsEnabled) { - if (!m_impl->_windowsAppUserModelId.empty()) - WinToast::instance()->setAppUserModelId(m_impl->_windowsAppUserModelId.c_str()); - else if (!m_impl->_notificationRegistrationId.empty()) - WinToast::instance()->setAppUserModelId(m_impl->_notificationRegistrationId.c_str()); - else - WinToast::instance()->setAppUserModelId(m_impl->_windowTitle.c_str()); + // WinToast is initialized at the application level. Only set the per-window app name. + WinToast::instance()->setAppName(m_impl->_windowTitle.c_str()); m_impl->_toastHandler = std::make_unique(this); - WinToast::instance()->initialize(); } m_impl->_dialog = std::make_unique(this); @@ -319,7 +274,9 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { Show(isAlreadyShown); } -InfiniFrameWindow::~InfiniFrameWindow() {} +InfiniFrameWindow::~InfiniFrameWindow() { + m_impl->_application->UntrackWindow(this); +} InfiniFrameWindowImpl* InfiniFrameWindow::ImplBase() noexcept { return m_impl.get(); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowLifecycle.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowLifecycle.Win32.cpp index 4c5ca00ba..20ebf9694 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowLifecycle.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowLifecycle.Win32.cpp @@ -25,29 +25,46 @@ void InfiniFrameWindow::Close() { PostMessage(m_impl->_hWnd, WM_CLOSE, 0, 0); } -void InfiniFrameWindow::WaitForExit() { - auto* impl = m_impl.get(); - ApplyPendingOwnerWindow(impl, L"wait_for_exit"); +void InfiniFrameWindow::MarkDestroyed() { + { + std::lock_guard lock(m_impl->_lifecycleMutex); + m_impl->_destroyed = true; + } + m_impl->_lifecycleClosed.notify_all(); +} - messageLoopRootWindowHandle = impl->_hWnd; - TraceTeardown(L"WaitForExit start instance=%p hwnd=%p", this, impl->_hWnd); +bool InfiniFrameWindow::IsDestroyed() const { + std::lock_guard lock(m_impl->_lifecycleMutex); + return m_impl->_destroyed; +} - MSG msg = {}; +void InfiniFrameWindow::WaitForExit() { + // Block until _destroyed is set. Use a timed wait so we can periodically + // yield to the OS scheduler, allowing WM_USER_INVOKE messages posted by + // other threads to be dispatched by the application's message loop or + // by our own message pump below. while (true) { - const int getMessageResult = GetMessage(&msg, nullptr, 0, 0); - if (getMessageResult == -1) { - TraceTeardown(L"WaitForExit GetMessage failed err=%lu", GetLastError()); - break; + { + std::unique_lock lock(m_impl->_lifecycleMutex); + if (m_impl->_destroyed) + return; + m_impl->_lifecycleClosed.wait_for(lock, std::chrono::milliseconds(50), [this] { + return m_impl->_destroyed; + }); + } + // Dispatch any pending messages for this thread to keep the + // message queue alive (needed for Invoke from other threads). + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + // Post WM_QUIT back so application loops can see it. + PostThreadMessage(GetCurrentThreadId(), WM_QUIT, 0, 0); + return; + } + TranslateMessage(&msg); + DispatchMessage(&msg); } - if (getMessageResult == 0) - break; - - TranslateMessage(&msg); - DispatchMessage(&msg); } - - messageLoopRootWindowHandle = nullptr; - TraceTeardown(L"WaitForExit end instance=%p hwnd=%p", this, impl->_hWnd); } namespace { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowProc.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowProc.Win32.cpp index f9d0e1213..b512b188c 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowProc.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowProc.Win32.cpp @@ -170,9 +170,8 @@ namespace { TraceTeardown(L"WM_DESTROY begin hwnd=%p instance=%p", hwnd, instance); instance->CloseWebView(); instance->InvokeClosed(); + instance->MarkDestroyed(); TraceTeardown(L"WM_DESTROY end hwnd=%p instance=%p", hwnd, instance); - if (hwnd == messageLoopRootWindowHandle) - PostQuitMessage(0); return 0; } @@ -307,8 +306,6 @@ LRESULT CALLBACK WindowProc(const HWND hwnd, const UINT uMsg, const WPARAM wPara case WM_DESTROY: { if (auto* instance = LookupWindowInstance(hwnd)) return handle_window_destruction(hwnd, instance, instance->m_impl.get()); - if (hwnd == messageLoopRootWindowHandle) - PostQuitMessage(0); return 0; } case WM_NCDESTROY: { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowState.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowState.Win32.cpp index 9513796ea..ac1bdff5e 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowState.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowState.Win32.cpp @@ -4,6 +4,7 @@ #include #include "Runtime/Platform/Windows/Window.Win32.Internal.h" +#include "Runtime/Shared/Application/InfiniFrameApplication.h" #include "Runtime/Shared/Utilities/StringCopy.h" // --------------------------------------------------------------------------------------------------------------------- // Code @@ -280,8 +281,6 @@ void InfiniFrameWindow::SetTitle(const char* title) { SetWindowText(m_impl->_hWnd, wideTitle.c_str()); if (m_impl->_notificationsEnabled) { WinToastLib::WinToast::instance()->setAppName(wideTitle.c_str()); - if (m_impl->_windowsAppUserModelId.empty() && m_impl->_notificationRegistrationId.empty()) - WinToastLib::WinToast::instance()->setAppUserModelId(wideTitle.c_str()); } } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Context.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Context.h index 52df07c36..3681ce137 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Context.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Context.h @@ -17,8 +17,6 @@ inline constexpr UINT WM_USER_INVOKE = WM_USER + 0x0002; inline constexpr UINT WM_USER_DISPATCH_OPERATION = WM_USER + 0x0003; -extern std::atomic _hInstance; -extern thread_local HWND messageLoopRootWindowHandle; extern const wchar_t* CLASS_NAME; struct InvokeWaitInfo { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Internal.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Internal.h index 9cbec8bdc..7ef2f1435 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Internal.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Window.Win32.Internal.h @@ -3,6 +3,8 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- #include +#include +#include #include #include #include @@ -21,8 +23,6 @@ struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { std::wstring _temporaryFilesPath; - std::wstring _notificationRegistrationId; - std::wstring _windowsAppUserModelId; bool _notificationsEnabled = false; std::string _defaultNotificationIcon; @@ -40,6 +40,11 @@ struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl { bool _useOsDefaultSize = false; bool _hasSavedRect = false; + // ── Lifecycle state (for WaitForExit when application owns message loop) ── + std::mutex _lifecycleMutex; + std::condition_variable _lifecycleClosed; + bool _destroyed = false; + RECT _savedRect = {}; int _lastLeft = INT_MIN; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/ApplicationInitParams.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/ApplicationInitParams.h new file mode 100644 index 000000000..f54541be5 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/ApplicationInitParams.h @@ -0,0 +1,24 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- + +/** + * @brief Initialization parameters for InfiniFrameApplication. + * + * Field order defines the ABI layout shared with the managed (.NET) side via LayoutKind.Sequential. + * When adding or removing fields, append at the end (before StructSize) and bump StructSize. + */ +struct ApplicationInitParams { + int StructSize; + + // ── Process identity (Win32) ────────────────────────────────────────── + const char* WindowsAppUserModelId; + const char* NotificationRegistrationId; + + // ── WebView2 runtime path override (Win32) ──────────────────────────── + const char* WebView2RuntimePath; + + // ── ABI version (must remain last) ──────────────────────────────────── + int Reserved; +}; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/InfiniFrameApplication.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/InfiniFrameApplication.h new file mode 100644 index 000000000..3cc8cf76b --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/InfiniFrameApplication.h @@ -0,0 +1,97 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#ifdef _WIN32 +#include +#endif +#ifdef __APPLE__ +#include +#endif +#ifdef __linux__ +#include +#endif + +#include +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +struct ApplicationInitParams; +class InfiniFrameWindow; + +/** + * @brief Application-level singleton managing platform registration, message loop, and window collection. + * + * One instance per process. Must be created before any windows and destroyed after all windows. + * Uses PIMPL idiom for platform-specific state encapsulation. + */ +class InfiniFrameApplication { + public: + /** + * @brief Construct a new InfiniFrameApplication. + * @param params Application initialization parameters. + */ + explicit InfiniFrameApplication(ApplicationInitParams* params); + + /** + * @brief Destroy InfiniFrameApplication. + */ + ~InfiniFrameApplication(); + + // ── Singleton access ─────────────────────────────────────────────── + /// Get the global application instance (null if none exists). + [[nodiscard]] static InfiniFrameApplication* GetInstance(); + + // ── Platform registration (one-time, called before any windows) ────── +#ifdef _WIN32 + /// Register the Win32 window class and set DPI awareness. + /// @param hInstance The application instance handle. + void Register(HINSTANCE hInstance); + + /// Get the stored HINSTANCE. + /// @return The HINSTANCE passed to Register(). + [[nodiscard]] HINSTANCE GetHInstance() const; + + /// Get the AppUserModelId set during construction. + [[nodiscard]] const std::wstring& GetAppUserModelId() const; + + /// Get the notification registration ID set during construction. + [[nodiscard]] const std::wstring& GetNotificationRegistrationId() const; + + /// Get the WebView2 runtime path set during construction. + [[nodiscard]] const std::wstring& GetWebView2RuntimePath() const; +#endif + +#ifdef __APPLE__ + /// Set up NSApplication delegate and activation policy. + void Register(); +#endif + + // ── Message loop ────────────────────────────────────────────────────── + /// Block until all windows are closed or Shutdown() is called. Runs the platform event loop. + void Run(); + + /// Signal the message loop to exit. Safe to call from any thread. + void Shutdown(); + + // ── Window management ───────────────────────────────────────────────── + /// Track a window as owned by this application. + void TrackWindow(InfiniFrameWindow* window); + + /// Remove a window from tracking. If no windows remain, triggers Shutdown(). + void UntrackWindow(InfiniFrameWindow* window); + + /// Check if any windows are still tracked. + [[nodiscard]] bool HasWindows() const; + + // ── Process-wide state ──────────────────────────────────────────────── + /// Check if Shutdown() has been called. + [[nodiscard]] bool IsShutdownRequested() const; + + private: + struct Impl; + std::unique_ptr m_impl; + static InfiniFrameApplication* s_instance; + + friend class InfiniFrameWindow; +}; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/InfiniFrameApplicationImpl.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/InfiniFrameApplicationImpl.h new file mode 100644 index 000000000..666789713 --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Application/InfiniFrameApplicationImpl.h @@ -0,0 +1,32 @@ +#pragma once +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +#include +#include +#include +#ifdef _WIN32 +#include +#include +#endif +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +class InfiniFrameWindow; +class InfiniFrameApplication; + +struct InfiniFrameApplicationImpl { + std::atomic _shutdownRequested{false}; + std::mutex _windowListMutex; + std::vector _windows; + +#ifdef _WIN32 + HINSTANCE _hInstance = nullptr; + std::wstring _appUserModelId; + std::wstring _notificationRegistrationId; + std::wstring _webView2RuntimePath; + DWORD _messageLoopThreadId = 0; +#endif +}; + +struct InfiniFrameApplication::Impl : InfiniFrameApplicationImpl {}; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameInitParams.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameInitParams.h index 1c7beb92b..686e0e80a 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameInitParams.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameInitParams.h @@ -28,9 +28,6 @@ struct InfiniFrameInitParams { const char* TemporaryFilesPath; const char* UserAgent; const char* BrowserControlInitParameters; - const char* WebView2RuntimePath; - const char* NotificationRegistrationId; - const char* WindowsAppUserModelId; const char* DefaultNotificationIcon; // ── Runtime configuration ────────────────────────────────────────────── @@ -108,6 +105,9 @@ struct InfiniFrameInitParams { // ── Menu ─────────────────────────────────────────────────────────────── const char* MenuBarJson; + // ── Application handle (new in v2) ───────────────────────────────────── + void* ApplicationHandle; + // ── ABI version (must remain last) ───────────────────────────────────── int StructSize; }; \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h index 4c8da0105..ee320334d 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h @@ -23,6 +23,7 @@ #include #endif +#include #include #include #include @@ -907,6 +908,16 @@ class InfiniFrameWindow { */ void InvokeFileDropped(const char** paths, int count, int x, int y) const noexcept; + // ----------------------------------------------------------------------------------------------------------------- + // Cross-platform lifecycle (for WaitForExit when application owns message loop) + // ----------------------------------------------------------------------------------------------------------------- + /// Mark the window as destroyed (called from platform-specific destroy handlers). + void MarkDestroyed(); + /// Returns true if the native window has been destroyed. + [[nodiscard]] bool IsDestroyed() const; + /// Block the calling thread until the native window is destroyed. + void WaitUntilDestroyed(); + // ----------------------------------------------------------------------------------------------------------------- // Platform-specific // ----------------------------------------------------------------------------------------------------------------- @@ -923,12 +934,6 @@ class InfiniFrameWindow { void OnWindowStateEvent(GdkWindowState newState); /// Flush any queued web messages that have not yet been delivered. void FlushPendingWebMessages(); - /// Mark the window as destroyed (called after the GTK widget is finalized). - void MarkDestroyed(); - /// Returns true if the native window has been destroyed. - bool IsDestroyed() const; - /// Block the calling thread until the native window is destroyed. - void WaitUntilDestroyed(); /** * @brief Get the native GTK toplevel window widget @@ -957,12 +962,6 @@ class InfiniFrameWindow { #endif #ifdef _WIN32 - /** - * @brief Register the Win32 window class; must be called once before creating any window - * @param hInstance Application instance handle - */ - static void Register(HINSTANCE hInstance); - /** * @brief Override the WebView2 fixed-version runtime path * @param pathToWebView2 UTF-8 path to the WebView2 runtime directory @@ -1026,11 +1025,6 @@ class InfiniFrameWindow { /// @param wParam The WPARAM containing the menu item identifier. void HandleMenuCommand(WPARAM wParam); #elif __APPLE__ - /** - * @brief Initialise the NSApplication shared instance; must be called once before creating any window - */ - static void Register(); - /// Flush any queued web messages that have not yet been delivered. void FlushPendingWebMessages(); /// Apply the current media autoplay configuration to the WKWebView. @@ -1087,6 +1081,10 @@ class InfiniFrameWindow { const InfiniFrameWindowImpl* ImplBase() const noexcept; std::unique_ptr m_impl; + // Guard flag set at the start of ~InfiniFrameWindow so that managed callbacks + // triggered during teardown can detect that destruction is in progress and + // avoid accessing m_impl after it has been destroyed. + std::atomic _destroying{false}; }; #include "InfiniFrameInitParams.h" \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindowImpl.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindowImpl.h index 6ee4873b2..acedf665f 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindowImpl.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindowImpl.h @@ -18,8 +18,12 @@ // Code // --------------------------------------------------------------------------------------------------------------------- class InfiniFrameWindow; +class InfiniFrameApplication; struct InfiniFrameWindowImpl { + // ── Application ownership ────────────────────────────────────────────── + InfiniFrameApplication* _application = nullptr; + std::mutex _operationMutex; std::unordered_map> _operations; std::mutex _navigationMutex; diff --git a/src/InfiniFrame.Shared/ApplicationConfiguration.cs b/src/InfiniFrame.Shared/ApplicationConfiguration.cs new file mode 100644 index 000000000..c71cc7e40 --- /dev/null +++ b/src/InfiniFrame.Shared/ApplicationConfiguration.cs @@ -0,0 +1,31 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +namespace InfiniFrame; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Configuration for an InfiniFrame application. +/// +public class ApplicationConfiguration { + /// + /// WINDOWS ONLY: The Win32 HINSTANCE handle. Required on Windows. + /// + public IntPtr HInstance { get; set; } + + /// + /// WINDOWS ONLY: Explicit application identity used by the taskbar for grouping and pinning. + /// + public string? WindowsAppUserModelId { get; set; } + + /// + /// WINDOWS ONLY: Registers the application for toast notifications. + /// + public string? NotificationRegistrationId { get; set; } + + /// + /// WINDOWS ONLY: Path to an extracted fixed-version WebView2 runtime. + /// + public string? WebView2RuntimePath { get; set; } +} diff --git a/src/InfiniFrame.Shared/IInfiniFrameApplication.cs b/src/InfiniFrame.Shared/IInfiniFrameApplication.cs new file mode 100644 index 000000000..ab63713d4 --- /dev/null +++ b/src/InfiniFrame.Shared/IInfiniFrameApplication.cs @@ -0,0 +1,87 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +namespace InfiniFrame; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Represents an InfiniFrame application singleton managing platform registration, message loop, and window collection. +/// One instance per process. Must be created before any windows and destroyed after all windows. +/// +public interface IInfiniFrameApplication : IDisposable, IAsyncDisposable { + /// Gets the unique identifier for this application instance. + Guid Id { get; } + + /// Gets the native application handle pointer. + IntPtr ApplicationHandle { get; } + + /// Gets whether Shutdown() has been called. + bool IsShutdownRequested { get; } + + /// + /// Initializes the application with the specified configuration. + /// Must be called before any windows are created. + /// + /// The application configuration. + void Initialize(ApplicationConfiguration config); + + /// + /// Runs the application message loop, blocking until all windows close or Shutdown() is called. + /// + void Run(); + + /// + /// Runs the application message loop asynchronously. + /// + /// A cancellation token to cancel the run operation. + /// A task that completes when the application exits. + Task RunAsync(CancellationToken ct = default); + + /// + /// Signals the application message loop to exit. Safe to call from any thread. + /// + void Shutdown(); + + /// + /// Closes all tracked windows gracefully. + /// Each window receives a close request; windows with closing callbacks may reject the close. + /// After all windows close, the application message loop exits automatically. + /// + void CloseAll(); + + /// + /// Registers a window to be built when Run() or RunAsync() is called. + /// The window is not created immediately β€” it is lazily built on the first Run/RunAsync call. + /// + /// A unique string identifier for the window. + /// A callback to configure the window builder. + void RegisterWindow(string id, Action configure); + + /// + /// Registers a window with an auto-generated GUID identifier. + /// + /// A callback to configure the window builder. + void RegisterWindow(Action configure); + + /// + /// Gets a previously registered window by its identifier. + /// Throws if Run() has not been called yet, or if the id is not found. + /// + /// The window identifier. + /// The window instance. + IInfiniFrameWindow GetWindow(string id); + + /// + /// Tries to get a previously registered window by its identifier. + /// Returns null if Run() has not been called, or if the id is not found. + /// + /// The window identifier. + /// The window instance, or null. + IInfiniFrameWindow? TryGetWindow(string id); + + /// + /// Gets all built windows. Empty until Run() is called. + /// + IReadOnlyList Windows { get; } +} diff --git a/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilder.cs b/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilder.cs index a427a575c..e0907b426 100644 --- a/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilder.cs +++ b/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilder.cs @@ -1,5 +1,5 @@ ο»Ώ// --------------------------------------------------------------------------------------------------------------------- -// Imports +// Code // --------------------------------------------------------------------------------------------------------------------- namespace InfiniFrame; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.Shared/Window/IInfiniFrameWindow.cs b/src/InfiniFrame.Shared/Window/IInfiniFrameWindow.cs index d89536448..69b86cc68 100644 --- a/src/InfiniFrame.Shared/Window/IInfiniFrameWindow.cs +++ b/src/InfiniFrame.Shared/Window/IInfiniFrameWindow.cs @@ -70,6 +70,7 @@ public interface IInfiniFrameWindow : IHasInfiniFrameEventsStore, INativeWindowH internal void MarkNativeHandleReleased(); internal void MarkDisposed(); internal void ReleaseNativeHandle(); + internal void MarkNativeHandleSafeToDestroy(); /// /// Updates the managed thread ID used for invoke dispatching. diff --git a/src/InfiniFrame.WebServer/InfiniFrameApplicationWebServerExtensions.cs b/src/InfiniFrame.WebServer/InfiniFrameApplicationWebServerExtensions.cs new file mode 100644 index 000000000..eb01a10c1 --- /dev/null +++ b/src/InfiniFrame.WebServer/InfiniFrameApplicationWebServerExtensions.cs @@ -0,0 +1,67 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using InfiniFrame.WebServer; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace InfiniFrame; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Extension methods for integrating ASP.NET Core web server with . +/// +public static class InfiniFrameApplicationWebServerExtensions { + /// + /// Adds an ASP.NET Core web server to the application. The web server starts before the + /// native window is created, so the window can navigate to the server's URL. + /// When the window closes, the web server is also stopped. + /// + /// The application instance. + /// Callback to configure the . + /// Optional callback to configure the window builder. + /// The for further configuration (e.g. UseAutoServerClose()). + public static InfiniFrameWebApplication WithWebServer( + this InfiniFrameApplication app, + Action configureWebApp, + Action? configureWindow = null + ) { + WebApplicationBuilder webAppBuilder = WebApplication.CreateBuilder(); + configureWebApp(webAppBuilder); + + // Build and start the web server before window creation. + WebApplication webApp = webAppBuilder.Build(); + webApp.UseDefaultFiles(); + webApp.Start(); + + // Create window builder and register with application. + var windowBuilder = new InfiniFrameWindowBuilder(); + configureWindow?.Invoke(windowBuilder); + + // Use a stable string key for the window so we can retrieve it later. + string windowId = $"webapp-{app.Id}"; + app.WithWindow(windowId, windowBuilder); + + // Build the wrapper. + var wrapper = new InfiniFrameWebApplication { + Logger = NullLogger.Instance, + WebApp = webApp, + LazyWindow = new Lazy(() => app.GetWindow(windowId)), + Application = app + }; + + // Auto-close the web server when the window closes. + windowBuilder.RegisterWindowClosingHandler((_, _) => StopWebApp(webApp)); + windowBuilder.RegisterWindowClosingRequestedHandler(_ => StopWebApp(webApp)); + + return wrapper; + } + + private static WindowClosingResult StopWebApp(WebApplication webApp) { + _ = webApp.StopAsync(CancellationToken.None); + return WindowClosingResult.Close; + } +} diff --git a/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs b/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs index 988b49218..bb8949dee 100644 --- a/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs +++ b/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs @@ -27,22 +27,12 @@ public class InfiniFrameWebApplication { public required Lazy LazyWindow { private get; init; } /// Gets the associated InfiniFrame window instance. public IInfiniFrameWindow Window => LazyWindow.Value; + /// Gets or sets the InfiniFrame application instance. + public IInfiniFrameApplication? Application { get; init; } // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- - /// - /// Creates a new with default ASP.NET Core and InfiniFrame - /// window builder services. - /// - /// Command-line arguments passed to the ASP.NET Core host builder. - /// An for further configuration. - public static InfiniFrameWebApplicationBuilder CreateBuilder(params string[] args) - => new InfiniFrameWebApplicationBuilder { - WebApp = WebApplication.CreateBuilder(args), - WindowBuilder = InfiniFrameWindowBuilder.Create() - }.Initialize(); - /// /// Runs the web application and window, blocking until the window is closed. /// @@ -61,8 +51,8 @@ public void Run() { try { // Wait until the host is accepting requests before creating the window. On Windows, - // WaitForClose owns the native message loop required by WebView2 initialization and - // navigation; WaitForCloseAsync only observes the closed signal. + // the application message loop is required by WebView2 initialization and navigation; + // WaitForCloseAsync only observes the closed signal. WebApp.StartAsync().GetAwaiter().GetResult(); Window.WaitForClose(); } @@ -113,9 +103,11 @@ public InfiniFrameWebApplication UseAutoServerClose() { return this; } - var builder = WebApp.Services.GetRequiredService(); - builder.RegisterWindowClosingHandler((_, _) => ClosingHandler()); - builder.RegisterWindowClosingRequestedHandler(_ => ClosingHandler()); + var builder = WebApp.Services.GetService(); + if (builder is not null) { + builder.RegisterWindowClosingHandler((_, _) => ClosingHandler()); + builder.RegisterWindowClosingRequestedHandler(_ => ClosingHandler()); + } return this; WindowClosingResult ClosingHandler() { diff --git a/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs b/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs index 6ec0d79db..4138bb6d2 100644 --- a/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs +++ b/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs @@ -23,12 +23,15 @@ public class InfiniFrameWebApplicationBuilder : IInfiniFrameWebApplicationBuilde // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- - internal InfiniFrameWebApplicationBuilder Initialize() { + public InfiniFrameWebApplicationBuilder Initialize(IInfiniFrameApplication? application = null) { Services .AddInfiniFrame() .AddSingleton(WindowBuilder) .AddSingleton(static provider => provider.GetRequiredService().Build(provider)); + if (application is not null) + Services.AddSingleton(application); + WebApp.WebHost.UseStaticWebAssets(); // Prefer ASPNETCORE_URLS, then "urls" else it has to be set by the dev themselves @@ -52,6 +55,14 @@ internal InfiniFrameWebApplicationBuilder Initialize() { /// /// The built . public InfiniFrameWebApplication Build() { + // Ensure core InfiniFrame services are registered even when Initialize() was not called. + if (!Services.Any(static s => s.ServiceType == typeof(IInfiniFrameApplication))) { + Services.AddInfiniFrame() + .AddSingleton(WindowBuilder) + .AddSingleton(static provider => provider.GetRequiredService().Build(provider)); + WindowBuilder.RegisterGetWebMessageHandler(); + } + WebApplication webApp = WebApp.Build(); webApp.UseDefaultFiles(); @@ -68,6 +79,16 @@ public InfiniFrameWebApplication Build() { configure: policyBuilder => policyBuilder.AddTrustedOrigin(baseUri)); } + // Initialize the application singleton so it's ready before any windows are created. + var application = webApp.Services.GetRequiredService(); + if (application.ApplicationHandle == IntPtr.Zero && !application.IsShutdownRequested) { + var appConfig = new ApplicationConfiguration(); + if (OperatingSystem.IsWindows()) { + appConfig.HInstance = System.Diagnostics.Process.GetCurrentProcess().MainModule?.BaseAddress ?? IntPtr.Zero; + } + application.Initialize(appConfig); + } + return new InfiniFrameWebApplication { Logger = webApp.Services.GetService>() ?? NullLogger.Instance, WebApp = webApp, diff --git a/src/InfiniFrame/Application/InfiniFrameApplication.cs b/src/InfiniFrame/Application/InfiniFrameApplication.cs new file mode 100644 index 000000000..8f1e5943d --- /dev/null +++ b/src/InfiniFrame/Application/InfiniFrameApplication.cs @@ -0,0 +1,495 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using System.Runtime.InteropServices; +using System.Text; +using InfiniFrame.NativeBridge; +using InfiniFrame.NativeBridge.Handles; +using InfiniFrame.NativeBridge.Parameters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace InfiniFrame; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Runtime implementation managing the native InfiniFrame application lifecycle including platform registration, +/// message loop execution, and window collection tracking. +/// Access the singleton via after calling . +/// +public sealed class InfiniFrameApplication( + ILogger logger +) : IInfiniFrameApplication { + private NativeApplicationHandle? _handle; + private ApplicationConfiguration? _configuration; + private int _disposed; + private readonly List<(string? Id, Action Configure)> _windowRegistrations = new(); + private readonly List<(string? Id, IInfiniFrameWindowBuilder Builder)> _directBuilders = new(); + private readonly Dictionary _builtWindows = new(); + private bool _built; + private Action? _onBeforeRun; + private IServiceProvider? _serviceProvider; + private readonly ServiceCollection _serviceCollection = new(); + + /// + /// Gets the current application instance. Only available after has been called. + /// + public static InfiniFrameApplication? Instance { get; internal set; } + + /// + public Guid Id { get; } = Guid.NewGuid(); + + /// + public IntPtr ApplicationHandle => _handle?.DangerousGetHandle() ?? IntPtr.Zero; + + /// + public bool IsShutdownRequested { get; private set; } + + /// + public IReadOnlyList Windows => _builtWindows.Values.ToList().AsReadOnly(); + + /// + /// Gets the service provider used by windows built by this application. + /// The provider is built on first access. Must be accessed after all services are registered. + /// + public IServiceProvider ServiceProvider => _serviceProvider ?? throw new InvalidOperationException( + "Service provider has not been built. Call Run() or access after window registration."); + + // ----------------------------------------------------------------------------------------------------------------- + // Static factory + // ----------------------------------------------------------------------------------------------------------------- + + /// + /// Creates and optionally initializes a new InfiniFrame application. + /// + /// Optional callback to configure application-level settings. + /// The application instance for fluent chaining with . + public static InfiniFrameApplication Initialize(Action? configure = null) { + var logger = NullLogger.Instance; + var app = new InfiniFrameApplication(logger); + + if (configure is null) return app; + + var config = new ApplicationConfiguration(); + configure(config); + app.Initialize(config); + + return app; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Fluent window registration + // ----------------------------------------------------------------------------------------------------------------- + + public InfiniFrameApplication WithWindow(Action configure) { + RegisterWindow(configure); + return this; + } + + /// + /// Registers a window with a unique string identifier. + /// The window is lazily built on the first Run() or RunAsync() call. + /// + /// A unique string identifier for the window. + /// A callback to configure the window builder. + /// The application instance for chaining. + public InfiniFrameApplication WithWindow(string id, Action configure) { + RegisterWindow(id, configure); + return this; + } + + /// + /// Registers a window with a unique string identifier using an existing builder. + /// The window is lazily built on the first Run() or RunAsync() call. + /// + /// A unique string identifier for the window. + /// The window builder to use. + /// The application instance for chaining. + public InfiniFrameApplication WithWindow(string id, IInfiniFrameWindowBuilder builder) { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(id); + ObjectDisposedException.ThrowIf(_disposed != 0, this); + if (_built) throw new InvalidOperationException("Cannot register windows after Run() has been called."); + + _directBuilders.Add((id, builder)); + return this; + } + + /// + /// Registers a window using an existing builder with an auto-generated GUID identifier. + /// + /// The window builder to use. + /// The application instance for chaining. + public InfiniFrameApplication WithWindow(IInfiniFrameWindowBuilder builder) { + ArgumentNullException.ThrowIfNull(builder); + ObjectDisposedException.ThrowIf(_disposed != 0, this); + if (_built) throw new InvalidOperationException("Cannot register windows after Run() has been called."); + + _directBuilders.Add((null, builder)); + return this; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Window management + // ----------------------------------------------------------------------------------------------------------------- + + /// + public void RegisterWindow(string id, Action configure) { + ArgumentNullException.ThrowIfNull(configure); + ArgumentException.ThrowIfNullOrWhiteSpace(id); + ObjectDisposedException.ThrowIf(_disposed != 0, this); + if (_built) throw new InvalidOperationException("Cannot register windows after Run() has been called."); + + _windowRegistrations.Add((id, configure)); + } + + /// + public void RegisterWindow(Action configure) { + ArgumentNullException.ThrowIfNull(configure); + ObjectDisposedException.ThrowIf(_disposed != 0, this); + if (_built) throw new InvalidOperationException("Cannot register windows after Run() has been called."); + + _windowRegistrations.Add((null, configure)); + } + + /// + public IInfiniFrameWindow GetWindow(string id) { + if (!_built) throw new InvalidOperationException("Windows have not been built yet. Call Run() or RunAsync() first."); + + return _builtWindows.TryGetValue(id, out IInfiniFrameWindow? window) + ? window + : throw new KeyNotFoundException($"Window with id '{id}' was not found."); + } + + /// + public IInfiniFrameWindow? TryGetWindow(string id) { + if (!_built) return null; + + return _builtWindows.TryGetValue(id, out IInfiniFrameWindow? window) ? window : null; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Methods + // ----------------------------------------------------------------------------------------------------------------- + + /// + public void Initialize(ApplicationConfiguration config) { + ObjectDisposedException.ThrowIf(_disposed != 0, this); + ArgumentNullException.ThrowIfNull(config); + + // Skip if already initialized β€” the first config wins. + if (_handle is not null) return; + + _configuration = config; + + var parameters = new ApplicationInitParameters { + StructSize = Marshal.SizeOf() + }; + + // Marshal string parameters + IntPtr appUserModelIdPtr = IntPtr.Zero; + IntPtr notificationRegIdPtr = IntPtr.Zero; + IntPtr webView2RuntimePathPtr = IntPtr.Zero; + + try { + if (config.WindowsAppUserModelId is not null) { + appUserModelIdPtr = MarshalStringUtf8(config.WindowsAppUserModelId); + parameters.WindowsAppUserModelId = appUserModelIdPtr; + } + + if (config.NotificationRegistrationId is not null) { + notificationRegIdPtr = MarshalStringUtf8(config.NotificationRegistrationId); + parameters.NotificationRegistrationId = notificationRegIdPtr; + } + + if (config.WebView2RuntimePath is not null) { + webView2RuntimePathPtr = MarshalStringUtf8(config.WebView2RuntimePath); + parameters.WebView2RuntimePath = webView2RuntimePathPtr; + } + + IntPtr unmanagedPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + try { + Marshal.StructureToPtr(parameters, unmanagedPtr, false); + + InfiniFrameNativeInteropStatus status = + InfiniFrameNative.ApplicationConstructor(unmanagedPtr, out IntPtr handle); + if (status != InfiniFrameNativeInteropStatus.Success) { + int lastError = Marshal.GetLastPInvokeError(); + string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; + throw new InfiniFrameNativeInteropException( + $"Application constructor failed with status {status}. Error #{lastError}. {nativeMessage}"); + } + + ArgumentOutOfRangeException.ThrowIfZero(handle); + + _handle = new NativeApplicationHandle(new IntPtr(handle)); + + logger.LogInformation("Native application initialized successfully."); + } + finally { + Marshal.FreeHGlobal(unmanagedPtr); + } + + // Platform-specific registration + if (OperatingSystem.IsWindows()) { + if (config.HInstance == IntPtr.Zero) + throw new InvalidOperationException("HInstance is required on Windows."); + + InfiniFrameNativeInteropStatus regStatus = + InfiniFrameNative.ApplicationRegisterWin32(_handle.DangerousGetHandle(), config.HInstance); + if (regStatus != InfiniFrameNativeInteropStatus.Success) { + int lastError = Marshal.GetLastPInvokeError(); + string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; + throw new InfiniFrameNativeInteropException( + $"Win32 registration failed with status {regStatus}. Error #{lastError}. {nativeMessage}"); + } + + logger.LogDebug("Win32 platform registration completed."); + } + else if (OperatingSystem.IsMacOS()) { + InfiniFrameNativeInteropStatus regStatus = + InfiniFrameNative.ApplicationRegisterMac(_handle.DangerousGetHandle()); + if (regStatus != InfiniFrameNativeInteropStatus.Success) { + int lastError = Marshal.GetLastPInvokeError(); + string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; + throw new InfiniFrameNativeInteropException( + $"macOS registration failed with status {regStatus}. Error #{lastError}. {nativeMessage}"); + } + + logger.LogDebug("macOS platform registration completed."); + } + else if (OperatingSystem.IsLinux()) { + logger.LogDebug("Linux GTK initialization handled natively."); + } + else { + throw new PlatformNotSupportedException(); + } + } + finally { + if (appUserModelIdPtr != IntPtr.Zero) Marshal.FreeHGlobal(appUserModelIdPtr); + if (notificationRegIdPtr != IntPtr.Zero) Marshal.FreeHGlobal(notificationRegIdPtr); + if (webView2RuntimePathPtr != IntPtr.Zero) Marshal.FreeHGlobal(webView2RuntimePathPtr); + } + + Instance = this; + + // Set up the shared service collection for windows built by this application. + // AddInfiniFrame() registers core services (events, configuration, validators, etc.) + // but skips IInfiniFrameApplication since it's already registered as 'this'. + _serviceCollection.AddInfiniFrame(); + _serviceCollection.AddSingleton(this); + _serviceCollection.AddTransient(); + } + + /// + /// Gets the service collection for this application. + /// Extensions (e.g., BlazorWebView) can add services here before Run() is called. + /// The service provider is built lazily on the first Run()/RunAsync() call. + /// + internal IServiceCollection ServiceCollection => _serviceCollection; + + /// + public void Run() { + ObjectDisposedException.ThrowIf(_disposed != 0, this); + if (_handle is null) + throw new InvalidOperationException("Application has not been initialized. Call Initialize() first."); + + _onBeforeRun?.Invoke(); + BuildAllWindows(); + + logger.LogDebug("Starting application message loop."); + + InfiniFrameNativeInteropStatus status = + InfiniFrameNative.ApplicationRun(_handle.DangerousGetHandle()); + if (status != InfiniFrameNativeInteropStatus.Success) { + int lastError = Marshal.GetLastPInvokeError(); + string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; + throw new InfiniFrameNativeInteropException( + $"Application run failed with status {status}. Error #{lastError}. {nativeMessage}"); + } + + logger.LogDebug("Application message loop exited."); + } + + /// + public async Task RunAsync(CancellationToken ct = default) { + ObjectDisposedException.ThrowIf(_disposed != 0, this); + if (_handle is null) + throw new InvalidOperationException("Application has not been initialized. Call Initialize() first."); + + _onBeforeRun?.Invoke(); + BuildAllWindows(); + + logger.LogDebug("Starting application message loop (async)."); + + await using CancellationTokenRegistration registration = ct.Register(() => Shutdown()); + + await Task.Run(action: () => { + InfiniFrameNativeInteropStatus status = + InfiniFrameNative.ApplicationRun(_handle.DangerousGetHandle()); + if (status != InfiniFrameNativeInteropStatus.Success) { + int lastError = Marshal.GetLastPInvokeError(); + string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; + throw new InfiniFrameNativeInteropException( + $"Application run failed with status {status}. Error #{lastError}. {nativeMessage}"); + } + }, ct).ConfigureAwait(false); + + logger.LogDebug("Application message loop exited (async)."); + } + + /// + public void Shutdown() { + if (_disposed != 0 || _handle is null) return; + + IsShutdownRequested = true; + + logger.LogDebug("Signaling application shutdown."); + + InfiniFrameNativeInteropStatus status = + InfiniFrameNative.ApplicationShutdown(_handle.DangerousGetHandle()); + if (status != InfiniFrameNativeInteropStatus.Success) { + int lastError = Marshal.GetLastPInvokeError(); + string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; + logger.LogWarning( + "Application shutdown signal returned status {Status}. Error #{LastError}. {Message}", + status, lastError, nativeMessage); + } + } + + /// + public void CloseAll() { + ObjectDisposedException.ThrowIf(_disposed != 0, this); + + logger.LogDebug("Closing all {WindowCount} windows.", _builtWindows.Count); + + foreach (KeyValuePair kvp in _builtWindows) { + IInfiniFrameWindow window = kvp.Value; + try { + window.Features.Lifecycle.Close(); + } + catch (Exception ex) { + logger.LogWarning(ex, "Failed to close window {WindowId}.", window.Id); + } + } + } + + /// + public void Dispose() { + Dispose(true); + } + + /// + public async ValueTask DisposeAsync() { + if (Interlocked.Exchange(ref _disposed, 1) != 0) return; + + foreach (IInfiniFrameWindow window in _builtWindows.Values) { + try { + switch (window) { + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + break; + case IDisposable disposable: + disposable.Dispose(); + break; + } + } + catch (Exception ex) { + logger.LogWarning(ex, "Failed to dispose window during application shutdown."); + } + } + + _builtWindows.Clear(); + + _handle?.Dispose(); + _handle = null; + if (Instance == this) Instance = null; + logger.LogDebug("Application disposed (async)."); + } + + private void Dispose(bool disposing) { + if (Interlocked.Exchange(ref _disposed, 1) != 0) return; + + if (disposing) { + foreach (IInfiniFrameWindow window in _builtWindows.Values) { + try { + if (window is IDisposable disposable) + disposable.Dispose(); + } + catch (Exception ex) { + logger.LogWarning(ex, "Failed to dispose window during application shutdown."); + } + } + + _builtWindows.Clear(); + + _handle?.Dispose(); + _handle = null; + if (Instance == this) Instance = null; + logger.LogDebug("Application disposed."); + } + } + + private void BuildAllWindows() { + if (_built) return; + + _built = true; + + // Merge builder services into the application's collection, then build the provider. + // Skip IInfiniFrameApplication registration β€” the application itself is the singleton. + ICollection serviceCollection = _serviceCollection; + foreach ((string? _, Action configure) in _windowRegistrations) { + var builder = new InfiniFrameWindowBuilder(); + configure(builder); + foreach (ServiceDescriptor descriptor in builder.Services) { + if (descriptor.ServiceType != typeof(IInfiniFrameApplication)) + serviceCollection.Add(descriptor); + } + } + + foreach ((string? _, IInfiniFrameWindowBuilder builder) in _directBuilders) { + if (builder is not InfiniFrameWindowBuilder concreteBuilder) continue; + + foreach (ServiceDescriptor descriptor in concreteBuilder.Services) { + if (descriptor.ServiceType != typeof(IInfiniFrameApplication)) + serviceCollection.Add(descriptor); + } + } + + // Build the shared provider now that all services are registered. + _serviceProvider = _serviceCollection.BuildServiceProvider(); + + // Now build the windows using the shared provider. + foreach ((string? id, Action configure) in _windowRegistrations) { + string windowId = id ?? Guid.NewGuid().ToString(); + var builder = new InfiniFrameWindowBuilder(); + configure(builder); + IInfiniFrameWindow window = builder.Build(ServiceProvider); + _builtWindows[windowId] = window; + } + + foreach ((string? id, IInfiniFrameWindowBuilder builder) in _directBuilders) { + string windowId = id ?? Guid.NewGuid().ToString(); + IInfiniFrameWindow window = builder.Build(ServiceProvider); + _builtWindows[windowId] = window; + } + + _windowRegistrations.Clear(); + _directBuilders.Clear(); + } + + internal void SetOnBeforeRun(Action action) { + _onBeforeRun = action; + } + + private static IntPtr MarshalStringUtf8(string? value) { + if (value is null) return IntPtr.Zero; + + byte[] utf8 = Encoding.UTF8.GetBytes(value + '\0'); + IntPtr ptr = Marshal.AllocHGlobal(utf8.Length); + Marshal.Copy(utf8, 0, ptr, utf8.Length); + return ptr; + } +} diff --git a/src/InfiniFrame/InfiniFrame.csproj.DotSettings b/src/InfiniFrame/InfiniFrame.csproj.DotSettings index f8c6b5249..81245773a 100644 --- a/src/InfiniFrame/InfiniFrame.csproj.DotSettings +++ b/src/InfiniFrame/InfiniFrame.csproj.DotSettings @@ -1,70 +1,37 @@ -ο»Ώ - True +ο»Ώ + True + True True - True + True True - True + True False - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True diff --git a/src/InfiniFrame/ServiceCollectionExtensions.cs b/src/InfiniFrame/ServiceCollectionExtensions.cs index 381016667..21e3799bd 100644 --- a/src/InfiniFrame/ServiceCollectionExtensions.cs +++ b/src/InfiniFrame/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using InfiniFrame.Interop; using InfiniFrame.NativeBridge.Parameters; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace InfiniFrame; // --------------------------------------------------------------------------------------------------------------------- @@ -15,15 +16,30 @@ namespace InfiniFrame; /// public static class ServiceCollectionExtensions { /// - /// Registers the core InfiniFrame services required for window management, events, and native interop. + /// Registers the core InfiniFrame services required for application management, window management, events, and native interop. /// /// The to add services to. + /// Optional callback to configure the application settings. /// The same service collection so calls can be chained. - public static IServiceCollection AddInfiniFrame(this IServiceCollection services) { + public static IServiceCollection AddInfiniFrame(this IServiceCollection services, Action? configure = null) { + // Only register IInfiniFrameApplication if not already registered. + // When using InfiniFrameApplication.Initialize().WithWebServer(), the application + // is created externally and added to DI before AddInfiniFrame() is called. + if (!services.Any(s => s.ServiceType == typeof(IInfiniFrameApplication))) { + services.AddSingleton(sp => { + var logger = sp.GetRequiredService>(); + var app = new InfiniFrameApplication(logger); + if (configure is not null) { + var config = new ApplicationConfiguration(); + configure(config); + app.Initialize(config); + } + return app; + }); + } services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddTransient(); services.AddSingleton, InfiniFrameNativeParametersValidator>(); services.AddSingleton(); diff --git a/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs b/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs index e79a0058c..ec0af59b8 100644 --- a/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs +++ b/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs @@ -16,7 +16,7 @@ namespace InfiniFrame; /// public class InfiniFrameWindowBuilder : IInfiniFrameWindowBuilder { - private IServiceCollection Services { get; init; } = new ServiceCollection().AddInfiniFrame(); + internal IServiceCollection Services { get; init; } = new ServiceCollection().AddLogging().AddInfiniFrame().AddTransient(); /// public IInfiniFrameWindowBuilderConfiguration Configuration { get; } = new InfiniFrameWindowBuilderConfiguration(); /// @@ -24,7 +24,7 @@ public class InfiniFrameWindowBuilder : IInfiniFrameWindowBuilder { /// public IDebuggingInfiniFrameWindowBuilderFeature Debugging => Features.Debugging; /// - public IInfiniFrameEventsStore EventsStore { get; private init; } = new InfiniFrameEventsStore(); + public IInfiniFrameEventsStore EventsStore { get; set; } = new InfiniFrameEventsStore(); /// public IInfiniFrameStaticAssets? StaticAssets { get; set; } @@ -39,6 +39,16 @@ public IInfiniFrameWindow Build(IServiceProvider? provider = null) { var featureFactory = actualProvider.GetRequiredService(); var validator = actualProvider.GetRequiredService>(); + // Ensure the application is initialized before creating any windows. + var application = actualProvider.GetRequiredService(); + if (application.ApplicationHandle == IntPtr.Zero && !application.IsShutdownRequested) { + var appConfig = new ApplicationConfiguration(); + if (OperatingSystem.IsWindows()) { + appConfig.HInstance = System.Diagnostics.Process.GetCurrentProcess().MainModule?.BaseAddress ?? IntPtr.Zero; + } + application.Initialize(appConfig); + } + InfiniFrameNativeParameters nativeParameters = CollectNativeParameters(); // Instance arbitration check @@ -48,7 +58,13 @@ public IInfiniFrameWindow Build(IServiceProvider? provider = null) { throw new InstanceAlreadyRunningException(); } - var window = actualProvider.GetRequiredService(); + // Create the window directly using ActivatorUtilities instead of resolving + // from the provider. This breaks the circular dependency where the lazy + // IInfiniFrameWindow factory (from InfiniFrameBlazorAppBuilder) calls + // Build(provider), which resolves IInfiniFrameWindow from the same provider. + var window = (InfiniFrameWindow)ActivatorUtilities.CreateInstance( + actualProvider, + typeof(InfiniFrameWindow)); window.SetOwnsServiceProvider(ownsServiceProvider); window.AssignFeatures(featureFactory.Create(window, this)); @@ -74,26 +90,6 @@ public IInfiniFrameWindow Build(IServiceProvider? provider = null) { } - // ----------------------------------------------------------------------------------------------------------------- - // Constructors - // ----------------------------------------------------------------------------------------------------------------- - /// - /// Creates a new with optional DI and event store overrides. - /// - /// Optional service collection. If null, a default collection with logging and InfiniFrame core services is created. - /// Optional pre-configured event store. If null, a new empty store is created. - /// A configured ready for feature configuration. - public static InfiniFrameWindowBuilder Create(IServiceCollection? collection = null, InfiniFrameEventsStore? events = null) { - var builder = new InfiniFrameWindowBuilder { - EventsStore = events ?? new InfiniFrameEventsStore(), - Services = (collection ?? new ServiceCollection()) - .AddLogging() - .AddInfiniFrame() - }; - - return builder; - } - internal InfiniFrameNativeParameters CollectNativeParameters() { var parameters = new InfiniFrameNativeParameters(); diff --git a/src/InfiniFrame/Window/Features/Browser/BrowserInfiniFrameWindowBuilderFeature.cs b/src/InfiniFrame/Window/Features/Browser/BrowserInfiniFrameWindowBuilderFeature.cs index 48534e807..a64caf7dc 100644 --- a/src/InfiniFrame/Window/Features/Browser/BrowserInfiniFrameWindowBuilderFeature.cs +++ b/src/InfiniFrame/Window/Features/Browser/BrowserInfiniFrameWindowBuilderFeature.cs @@ -177,6 +177,5 @@ public void ApplyToNativeParameters(ref InfiniFrameNativeParameters parameters) parameters.BrowserShortcutsEnabled = IsBrowserShortcutsEnabled; parameters.BrowserControlInitParameters = BrowserControlInitParameters; parameters.TemporaryFilesPath = TemporaryFilesPath; - parameters.WebView2RuntimePath = WebView2RuntimePath; } } diff --git a/src/InfiniFrame/Window/Features/Decorations/DecorationsInfiniFrameWindowBuilderFeature.cs b/src/InfiniFrame/Window/Features/Decorations/DecorationsInfiniFrameWindowBuilderFeature.cs index 7d28b0602..d8b929a75 100644 --- a/src/InfiniFrame/Window/Features/Decorations/DecorationsInfiniFrameWindowBuilderFeature.cs +++ b/src/InfiniFrame/Window/Features/Decorations/DecorationsInfiniFrameWindowBuilderFeature.cs @@ -79,8 +79,6 @@ public void ApplyToNativeParameters(ref InfiniFrameNativeParameters parameters) parameters.WindowIconFile = IconFileUtility.TryResolveIconFilePath(IconFilePath, out string? resolvedIconFilePath) ? resolvedIconFilePath : null; - parameters.WindowsAppUserModelId = WindowsAppUserModelId; - ColorUtility.ParseBackgroundColor( BackgroundColor, out byte r, out byte g, out byte b, out byte a); parameters.BackgroundColorR = r; diff --git a/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs index 4fddd60a3..a5bdd5501 100644 --- a/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs @@ -20,6 +20,7 @@ namespace InfiniFrame; /// public class LifecycleInfiniFrameWindowFeature( IInfiniFrameWindow window, + IInfiniFrameApplication application, ILogger logger, IValidator validator ) : ILifecycleInfiniFrameWindowFeature, IDisposable { @@ -108,18 +109,9 @@ void ILifecycleInfiniFrameWindowFeature.Initialize() { window.Events.OnWindowCreating(); try { - if (OperatingSystem.IsWindows()) InfiniFrameNative.RegisterWin32(window.MainProgramHandle); - else if (OperatingSystem.IsMacOS()) { - InfiniFrameNativeInteropStatus registerStatus = InfiniFrameNative.RegisterMac(); - if (registerStatus != InfiniFrameNativeInteropStatus.Success) { - int lastError = Marshal.GetLastPInvokeError(); - string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; - throw new InfiniFrameNativeInteropException( - $"Native registration failed with status {registerStatus}. Error #{lastError}. {nativeMessage}"); - } - } - else if (OperatingSystem.IsLinux()) {}// No specific implementation for Linux - else throw new PlatformNotSupportedException(); + // Platform registration is now handled by the application. + // Pass the application handle to the native window constructor. + startupParameters.ApplicationHandle = application.ApplicationHandle; using NativeHandleLease? parentLease = window.Configuration.ParentWindow is {} parent ? parent.AcquireNativeHandle() @@ -362,12 +354,48 @@ private void Dispose(bool disposing) { if (window.LifecycleState < InfiniFrameWindowLifecycleState.TeardownComplete && window.LifecycleState != InfiniFrameWindowLifecycleState.Disposed) { - // The normal teardown path hasn't completed yet. Still release callback roots - // and milestones to avoid leaks, and release the native handle so .NET 10's - // runtime doesn't abort during shutdown over unreleased SafeHandles. + // The normal teardown path hasn't completed yet. The native + // ScheduleTeardownCompletion (WM_NCDESTROY) queued a thread pool + // work item that accesses the C++ object via a raw this pointer. + // We must not free the C++ object until that work item finishes. + // + // Finalizer path (disposing == false): The GC finalizer runs on a + // dedicated thread and must not block waiting for the thread pool. + // Release only managed callback roots/markers here; the native + // handle itself is released by the explicit Dispose(true) path + // after waiting for _teardown, or by the NativeWindowHandle + // finalizer as a last resort. + if (!disposing) { + ReleaseNativeCallbackRootOnce(); + ReleaseMilestoneRootOnce(); + try { window.MarkDisposed(); } + catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { + logger.LogWarning(ex, "MarkDisposed failed during finalization"); + } + + return; + } + + // Explicit Dispose path: wait for the teardown thread pool work + // item to complete before releasing the native handle, preventing + // use-after-free of the C++ object. Safe to block when the message + // loop has already exited or we are not on the owning STA thread. + if (Volatile.Read(ref _messageLoopExited) != 0 + || Environment.CurrentManagedThreadId != window.ManagedThreadId) { + try { + _teardown.Task.GetAwaiter().GetResult(); + } + catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { + logger.LogTrace(ex, "Teardown wait failed during emergency disposal."); + } + } + ReleaseNativeCallbackRootOnce(); ReleaseMilestoneRootOnce(); - try { window.ReleaseNativeHandle(); } + try { + window.MarkNativeHandleSafeToDestroy(); + window.ReleaseNativeHandle(); + } catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { logger.LogWarning(ex, "ReleaseNativeHandle failed during disposal"); } @@ -392,6 +420,7 @@ private void CleanupClosedHandleAndCallbacks(bool disposing) { if (Interlocked.Exchange(ref _cleanupCompleted, 1) != 0) return; try { + window.MarkNativeHandleSafeToDestroy(); window.ReleaseNativeHandle(); window.MarkNativeHandleReleased(); window.MarkDisposed(); diff --git a/src/InfiniFrame/Window/Features/Notifications/NotificationsInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/Notifications/NotificationsInfiniFrameWindowFeature.cs index c305ef4c8..51fa8c455 100644 --- a/src/InfiniFrame/Window/Features/Notifications/NotificationsInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/Notifications/NotificationsInfiniFrameWindowFeature.cs @@ -20,11 +20,6 @@ public class NotificationsInfiniFrameWindowFeature( ILogger logger ) : INotificationsInfiniFrameWindowFeature { - /// - /// Gets the notification registration identifier from the window's startup parameters. - /// - public string? NotificationRegistrationId => window.Configuration.StartupParameters.NotificationRegistrationId; - /// /// Gets whether desktop notifications are enabled for this window. /// diff --git a/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs index 6bd28c255..3920c8b39 100644 --- a/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs @@ -121,10 +121,13 @@ private async ValueTask SendLocallyAsync(string message, CancellationToken ct) { cancellationToken: ct ).ConfigureAwait(false); - if (result == InfiniFrameDispatchResult.Cancelled) - throw new OperationCanceledException(ct); - if (result is InfiniFrameDispatchResult.Failed or InfiniFrameDispatchResult.TimedOut) - throw new InvalidOperationException($"Web-message submission ended with {result}."); + switch (result) + { + case InfiniFrameDispatchResult.Cancelled: + throw new OperationCanceledException(ct); + case InfiniFrameDispatchResult.Failed or InfiniFrameDispatchResult.TimedOut: + throw new InvalidOperationException($"Web-message submission ended with {result}."); + } } private static string CreateAcknowledgementPayload(ulong id, string message) { diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index d5b0015ad..2319cf0a7 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -177,14 +177,18 @@ void IInfiniFrameWindow.AssignNativeHandle(IntPtr handle) { safeHandle?.Dispose(); } - if (LifecycleState == InfiniFrameWindowLifecycleState.Creating) return; - - // A very early native closed callback won the transition. Keep ownership - // for deferred teardown but never resurrect the window back to Running. - if (LifecycleState >= InfiniFrameWindowLifecycleState.NativeClosed) return; + switch (LifecycleState) + { + case InfiniFrameWindowLifecycleState.Creating: + // A very early native closed callback won the transition. Keep ownership + // for deferred teardown but never resurrect the window back to Running. + case >= InfiniFrameWindowLifecycleState.NativeClosed: + return; + default: + Interlocked.Exchange(ref _instanceHandle, null).Dispose(); + throw new InvalidOperationException($"Cannot assign a native handle in state {LifecycleState}."); + } - Interlocked.Exchange(ref _instanceHandle, null).Dispose(); - throw new InvalidOperationException($"Cannot assign a native handle in state {LifecycleState}."); } void IInfiniFrameWindow.MarkReady() { @@ -287,6 +291,10 @@ void IInfiniFrameWindow.ReleaseNativeHandle() { handle?.Dispose(); } + void IInfiniFrameWindow.MarkNativeHandleSafeToDestroy() { + Volatile.Read(ref _instanceHandle)?.MarkSafeToDestroy(); + } + public NativeHandleLease AcquireNativeHandle(NativeHandleAccess access = NativeHandleAccess.Feature) { InfiniFrameWindowLifecycleState state = LifecycleState; bool allowed = access switch { diff --git a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs index 7068e56b9..1d2a2691c 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs @@ -41,6 +41,7 @@ public IInfiniFrameWindowFeatures Create(IInfiniFrameWindow window, IInfiniFrame ), new LifecycleInfiniFrameWindowFeature( window, + provider.GetRequiredService(), GetLogger(provider), provider.GetRequiredService>() ), diff --git a/tests/InfiniAutomationTests.BlazorWebView.MudBlazor/CustomElementsTests.cs b/tests/InfiniAutomationTests.BlazorWebView.MudBlazor/CustomElementsTests.cs index 28e9641ce..390ae2f1d 100644 --- a/tests/InfiniAutomationTests.BlazorWebView.MudBlazor/CustomElementsTests.cs +++ b/tests/InfiniAutomationTests.BlazorWebView.MudBlazor/CustomElementsTests.cs @@ -16,6 +16,7 @@ public sealed class CustomElementsTests : InfiniFramePlaywrightTestBase { [Test] [NotInParallelInfiniAutomationTests] + [Timeout(30_000)] public async Task CustomElement_Registers_Renders_AndUpdatesFromAttributes(CancellationToken ct = default) { IPage page = await GetRootPageAsync(); @@ -84,6 +85,7 @@ await EvaluateWhenPageReadyAsync( [Test] [NotInParallelInfiniAutomationTests] + [Timeout(30_000)] public async Task JsComponent_WithoutInitializer_AutoRegisters_AsCustomElement_ByDefault(CancellationToken ct = default) { IPage page = await GetRootPageAsync(); diff --git a/tests/InfiniAutomationTests/TestUtility/BlazorPlaywrightContextBase.cs b/tests/InfiniAutomationTests/TestUtility/BlazorPlaywrightContextBase.cs index 9176f3883..e116be946 100644 --- a/tests/InfiniAutomationTests/TestUtility/BlazorPlaywrightContextBase.cs +++ b/tests/InfiniAutomationTests/TestUtility/BlazorPlaywrightContextBase.cs @@ -99,7 +99,7 @@ private Thread CreateAppThread(TaskCompletionSource ready) { private void RunAppOnThread(TaskCompletionSource ready) { try { - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); ConfigureServices(builder.Services); ConfigureRootComponents(builder.RootComponents); @@ -115,13 +115,7 @@ private void RunAppOnThread(TaskCompletionSource ready) { RunApp(app); } - catch (InvalidOperationException ex) { - ready.TrySetException(ex); - } - catch (TimeoutException ex) { - ready.TrySetException(ex); - } - catch (PlaywrightException ex) { + catch (Exception ex) { ready.TrySetException(ex); } } diff --git a/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs b/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs index b9ac5c782..c80eb4a29 100644 --- a/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs +++ b/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilderTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using System.Reflection; @@ -25,7 +25,7 @@ public class InfiniFrameBlazorAppBuilderTests { [Test] public async Task Build_WithExternalProvider_ShouldUseProvidedServiceProvider(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); ServiceProvider serviceProvider = builder.Services.BuildServiceProvider(); // Act @@ -39,7 +39,7 @@ public async Task Build_WithExternalProvider_ShouldUseProvidedServiceProvider(Ca [Test] public async Task Build_WithoutProvider_ShouldCreateServiceProvider(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); // Act InfiniFrameBlazorApp app = builder.Build(); @@ -52,7 +52,7 @@ public async Task Build_WithoutProvider_ShouldCreateServiceProvider(Cancellation [Test] public async Task CreateDefault_RootComponents_ImplementsIJsComponentConfiguration(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); // Act IJSComponentConfiguration configuration = builder.RootComponents; @@ -64,7 +64,7 @@ public async Task CreateDefault_RootComponents_ImplementsIJsComponentConfigurati [Test] public async Task CreateDefault_RootComponents_RegisterForJavaScript_WritesToSharedStore(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); // Act builder.RootComponents.RegisterForJavaScript("test-js-component"); @@ -82,7 +82,7 @@ public async Task CreateDefault_RootComponents_RegisterForJavaScript_WritesToSha public async Task GlobalUnhandledExceptionHandler_IsRemovedOnDispose(CancellationToken ct = default) { // Arrange var recordingSource = new RecordingUnhandledExceptionSource(); - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); builder.Services.RemoveAll(); builder.Services.AddSingleton(recordingSource); @@ -99,7 +99,7 @@ public async Task GlobalUnhandledExceptionHandler_IsRemovedOnDispose(Cancellatio public async Task GlobalUnhandledExceptionHandler_RepeatedBuildDispose_DoesNotAccumulate(CancellationToken ct = default) { // Arrange var recordingSource = new RecordingUnhandledExceptionSource(); - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); builder.Services.RemoveAll(); builder.Services.AddSingleton(recordingSource); var activeBeforeDispose = new List(); @@ -123,7 +123,7 @@ public async Task GlobalUnhandledExceptionHandler_RepeatedBuildDispose_DoesNotAc public async Task GlobalUnhandledExceptionHandler_CanBeDisabled(CancellationToken ct = default) { // Arrange var recordingSource = new RecordingUnhandledExceptionSource(); - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); builder.Services.RemoveAll(); builder.Services.AddSingleton(recordingSource); builder.Services.Configure(options => options.EnableGlobalUnhandledExceptionHandler = false); @@ -140,7 +140,7 @@ public async Task GlobalUnhandledExceptionHandler_CanBeDisabled(CancellationToke [Test] public async Task CreateDefault_RegistersUnhandledExceptionSourceByDefault(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); ServiceProvider serviceProvider = builder.Services.BuildServiceProvider(); // Act @@ -153,7 +153,7 @@ public async Task CreateDefault_RegistersUnhandledExceptionSourceByDefault(Cance [Test] public async Task CreateDefault_ExceptionSourceRejectsNullHandler(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var builder = new InfiniFrameBlazorAppBuilder(); ServiceProvider serviceProvider = builder.Services.BuildServiceProvider(); var source = serviceProvider.GetRequiredService(); @@ -175,7 +175,8 @@ public async Task SetBrowserControlInitParameters_ThroughCreateDefault_ShouldWor const string initParameters = "--force-device-scale-factor=1"; // Act - var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args, windowBuilder: builder => builder + var appbuilder = new InfiniFrameBlazorAppBuilder(); + appbuilder.WindowBuilder .SetTitle("Test") .SetBrowserControlInitParameters(initParameters) .SetLeft(0) @@ -183,8 +184,7 @@ public async Task SetBrowserControlInitParameters_ThroughCreateDefault_ShouldWor .SetSize(100, 100) .SetResizable(false) .SetChromeless() - .EnableSmoothScrolling(false) - ); + .EnableSmoothScrolling(false); // Assert await Assert.That(appbuilder).IsNotNull(); @@ -201,7 +201,7 @@ public async Task SetBrowserControlInitParameters_ThroughAppBuilder_ShouldWork(C const string initParameters = "--force-device-scale-factor=1"; // Act - var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args); + var appbuilder = new InfiniFrameBlazorAppBuilder(); appbuilder.WindowBuilder .SetTitle("Test") .SetBrowserControlInitParameters(initParameters) @@ -222,6 +222,7 @@ await Assert.That(appbuilder.WindowBuilder.Features.Browser.BrowserControlInitPa [Test] [NotInParallelInfiniTests] [SkipOnLinux("Given init parameters are not supported on Linux")] + [Timeout(30_000)] public async Task SetBrowserControlInitParameters_ThroughCreateDefault_ShouldWorkOnWindow(CancellationToken ct = default) { // Arrange string[] args = []; @@ -230,7 +231,8 @@ public async Task SetBrowserControlInitParameters_ThroughCreateDefault_ShouldWor : "--force-device-scale-factor=1"; // Act - var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args, windowBuilder: builder => builder + var appbuilder = new InfiniFrameBlazorAppBuilder(); + appbuilder.WindowBuilder .SetTitle("Test") .SetBrowserControlInitParameters(initParameters) .SetLeft(0) @@ -238,8 +240,7 @@ public async Task SetBrowserControlInitParameters_ThroughCreateDefault_ShouldWor .SetSize(100, 100) .SetResizable(false) .SetChromeless() - .EnableSmoothScrolling(false) - ); + .EnableSmoothScrolling(false); InfiniFrameBlazorApp app = appbuilder.Build(); var window = app.ServiceProvider.GetRequiredService(); @@ -255,6 +256,7 @@ await Assert.That(window.Configuration.StartupParameters.BrowserControlInitParam [Test] [NotInParallelInfiniTests] [SkipOnLinux("Given init parameters are not supported on Linux")] + [Timeout(30_000)] public async Task SetBrowserControlInitParameters_ThroughAppBuilder_ShouldWorkOnWindow(CancellationToken ct = default) { // Arrange string[] args = []; @@ -263,7 +265,7 @@ public async Task SetBrowserControlInitParameters_ThroughAppBuilder_ShouldWorkOn : "--force-device-scale-factor=1"; // Act - var appbuilder = InfiniFrameBlazorAppBuilder.CreateDefault(args); + var appbuilder = new InfiniFrameBlazorAppBuilder(); appbuilder.WindowBuilder .SetTitle("Test") .SetBrowserControlInitParameters(initParameters) @@ -287,9 +289,10 @@ await Assert.That(window.Configuration.StartupParameters.BrowserControlInitParam [Test] [NotInParallelInfiniTests] + [Timeout(30_000)] public async Task Build_SetsStartupUrlToAppBaseForDefaultHostPage(CancellationToken ct = default) { // Arrange - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var appBuilder = new InfiniFrameBlazorAppBuilder(); // Act InfiniFrameBlazorApp app = appBuilder.Build(); @@ -302,8 +305,9 @@ public async Task Build_SetsStartupUrlToAppBaseForDefaultHostPage(CancellationTo [Test] [NotInParallelInfiniTests] + [Timeout(30_000)] public async Task Build_TrustsAppOriginForFragmentNavigation(CancellationToken ct = default) { - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var appBuilder = new InfiniFrameBlazorAppBuilder(); await using InfiniFrameBlazorApp app = appBuilder.Build(); IInfiniFrameUriSecurityPolicy policy = InfiniFrameUriSecurityPolicyRegistry.GetForBuilder(appBuilder.WindowBuilder); @@ -314,9 +318,10 @@ public async Task Build_TrustsAppOriginForFragmentNavigation(CancellationToken c [Test] [NotInParallelInfiniTests] + [Timeout(30_000)] public async Task Build_SetsStartupUrlToConfiguredNonDefaultHostPage(CancellationToken ct = default) { // Arrange - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var appBuilder = new InfiniFrameBlazorAppBuilder(); appBuilder.Services.Configure(options => { options.HostPage = "shell/host.html"; }); @@ -332,9 +337,10 @@ public async Task Build_SetsStartupUrlToConfiguredNonDefaultHostPage(Cancellatio [Test] [NotInParallelInfiniTests] + [Timeout(30_000)] public async Task Build_SetsWindowBuilderStaticAssets(CancellationToken ct = default) { // Arrange - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var appBuilder = new InfiniFrameBlazorAppBuilder(); // Act InfiniFrameBlazorApp app = appBuilder.Build(); @@ -348,9 +354,10 @@ public async Task Build_SetsWindowBuilderStaticAssets(CancellationToken ct = def [Test] [NotInParallelInfiniTests] + [Timeout(30_000)] public async Task Build_PopulatesNativeStartupCustomSchemeCallback(CancellationToken ct = default) { // Arrange - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var appBuilder = new InfiniFrameBlazorAppBuilder(); // Act InfiniFrameBlazorApp app = appBuilder.Build(); @@ -377,7 +384,7 @@ public async Task Build_ExposesDebuggingThroughWindowFeatures(CancellationToken window.Features.Returns(features.Object); window.Debugging.Returns(debuggingFeature.Object); - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var appBuilder = new InfiniFrameBlazorAppBuilder(); appBuilder.Services.RemoveAll(); appBuilder.Services.AddSingleton(window.Object); diff --git a/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppTeardownTests.cs b/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppTeardownTests.cs index fd8f5b35a..42ae0a2b6 100644 --- a/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppTeardownTests.cs +++ b/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameBlazorAppTeardownTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using System.Runtime.Versioning; @@ -16,6 +16,7 @@ public sealed class InfiniFrameBlazorAppTeardownTests { [OnlyRunOnWindowsX64] [NotInParallelInfiniTests] [SupportedOSPlatform("windows")] + [Timeout(45_000)] public async Task Run_WindowClosed_CompletesRendererDisposal(CancellationToken ct = default) { // Arrange var windowReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -23,7 +24,7 @@ public async Task Run_WindowClosed_CompletesRendererDisposal(CancellationToken c var thread = new Thread(() => { try { - var appBuilder = InfiniFrameBlazorAppBuilder.CreateDefault(); + var appBuilder = new InfiniFrameBlazorAppBuilder(); appBuilder.RootComponents.Add("app"); InfiniFrameBlazorApp app = appBuilder.Build(); diff --git a/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameWebViewManagerTests.cs b/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameWebViewManagerTests.cs index eb219093f..1c22822e7 100644 --- a/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameWebViewManagerTests.cs +++ b/tests/InfiniTests.InfiniFrame.BlazorWebView/InfiniFrameWebViewManagerTests.cs @@ -26,7 +26,7 @@ public class InfiniFrameWebViewManagerTests { public async Task HandleWebRequest_FragmentAndQueryAreExcludedFromLookup(CancellationToken ct = default) { byte[] expected = [.. "settings-page"u8]; var fileProvider = new RecordingFileProvider("index.html", expected); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); await using ServiceProvider provider = new ServiceCollection().AddLogging().BuildServiceProvider(); await using var manager = new TestableInfiniFrameWebViewManager( builder, @@ -58,7 +58,7 @@ public async Task HandleWebRequest_MalformedOrUntrustedUrlIsRejected(string url, var fileProvider = new RecordingFileProvider("index.html", [.. "blocked"u8]); await using ServiceProvider provider = new ServiceCollection().AddLogging().BuildServiceProvider(); await using var manager = new TestableInfiniFrameWebViewManager( - InfiniFrameWindowBuilder.Create(), + new InfiniFrameWindowBuilder(), provider, MockFactory.CreateDispatcherMock().Object, fileProvider, @@ -92,7 +92,7 @@ public async Task SendMessage_AfterDispose_ShouldReturnPromptly(CancellationToke Dispatcher dispatcher = MockFactory.CreateDispatcherMock().Object; var manager = new TestableInfiniFrameWebViewManager( - InfiniFrameWindowBuilder.Create(), + new InfiniFrameWindowBuilder(), provider, dispatcher, new NullFileProvider(), @@ -143,7 +143,7 @@ public async Task SendMessage_ShouldSerializeOutgoingMessages(CancellationToken .BuildServiceProvider(); var manager = new TestableInfiniFrameWebViewManager( - InfiniFrameWindowBuilder.Create(), + new InfiniFrameWindowBuilder(), provider, MockFactory.CreateDispatcherMock().Object, new NullFileProvider(), @@ -183,8 +183,15 @@ public async Task SendMessage_WhenBoundedQueueIsFull_ShouldApplyConfiguredBackpr webMessagingMock.SendWebMessageAsync(Any(), Any()) .Callback((message, _) => { sentMessages.Add(message); - if (message == "first") firstStarted.TrySetResult(true); - if (message == "second") secondDelivered.TrySetResult(true); + switch (message) + { + case "first": + firstStarted.TrySetResult(true); + break; + case "second": + secondDelivered.TrySetResult(true); + break; + } }).Returns(() => backpressureReturnValue); await using ServiceProvider provider = new ServiceCollection() @@ -281,7 +288,7 @@ private static TestableInfiniFrameWebViewManager CreateManager( IServiceProvider provider, InfiniFrameBlazorAppConfiguration? configuration = null ) => new( - InfiniFrameWindowBuilder.Create(), + new InfiniFrameWindowBuilder(), provider, MockFactory.CreateDispatcherMock().Object, new NullFileProvider(), diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs index ee455927c..042e9d5f0 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs @@ -87,9 +87,6 @@ public async Task ReturnAsIsIsValid(CancellationToken ct = default) { TemporaryFilesPath = "temp", UserAgent = "agent name", BrowserControlInitParameters = "some params", - WebView2RuntimePath = "C:\\WebView2Runtime", - NotificationRegistrationId = "some id", - WindowsAppUserModelId = "InfiniLore.InfiniFrame.Tests", RemoteDebuggingPort = 9222, NativeParent = new IntPtr(87654321), CustomSchemeNames = customSchemeNames, @@ -173,9 +170,6 @@ public async Task ReturnAsIsIsValid(CancellationToken ct = default) { await Assert.That(newParameters.TemporaryFilesPath).IsEqualTo(parameters.TemporaryFilesPath); await Assert.That(newParameters.UserAgent).IsEqualTo(parameters.UserAgent); await Assert.That(newParameters.BrowserControlInitParameters).IsEqualTo(parameters.BrowserControlInitParameters); - await Assert.That(newParameters.WebView2RuntimePath).IsEqualTo(parameters.WebView2RuntimePath); - await Assert.That(newParameters.NotificationRegistrationId).IsEqualTo(parameters.NotificationRegistrationId); - await Assert.That(newParameters.WindowsAppUserModelId).IsEqualTo(parameters.WindowsAppUserModelId); await Assert.That(newParameters.RemoteDebuggingPort).IsEqualTo(parameters.RemoteDebuggingPort); await Assert.That(newParameters.NativeParent).IsEqualTo(parameters.NativeParent); await Assert.That(newParameters.Left).IsEqualTo(parameters.Left); diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidatorTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidatorTests.cs index a92dd648d..e8dff6925 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidatorTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidatorTests.cs @@ -94,34 +94,6 @@ public async Task Validate_BoundaryValues_PassesWhenInRange(int width, int heigh await Assert.That(result.IsValid).IsTrue(); } - [Test] - [Arguments("")] - [Arguments("InfiniLore Invalid")] - public async Task Validate_InvalidWindowsAppUserModelId_FailsValidation( - string value, - CancellationToken ct = default - ) { - InfiniFrameNativeParameters parameters = CreateValidParameters(); - parameters.WindowsAppUserModelId = value; - - ValidationResult result = await Validator.ValidateAsync(parameters, ct); - - await Assert.That(result.IsValid).IsFalse(); - await Assert.That(result.Errors.Any( - error => error.PropertyName == nameof(InfiniFrameNativeParameters.WindowsAppUserModelId) - )).IsTrue(); - } - - [Test] - public async Task Validate_TooLongWindowsAppUserModelId_FailsValidation(CancellationToken ct = default) { - InfiniFrameNativeParameters parameters = CreateValidParameters(); - parameters.WindowsAppUserModelId = new string('a', 129); - - ValidationResult result = await Validator.ValidateAsync(parameters, ct); - - await Assert.That(result.IsValid).IsFalse(); - } - private static InfiniFrameNativeParameters CreateValidParameters() => new() { StartUrl = "https://example.com", diff --git a/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationBuilderTests.cs b/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationBuilderTests.cs index 396ecab23..d5a37798b 100644 --- a/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationBuilderTests.cs +++ b/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationBuilderTests.cs @@ -4,6 +4,7 @@ using InfiniFrame; using InfiniFrame.Security; using InfiniFrame.WebServer; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace InfiniTests.InfiniFrame.WebServer; @@ -12,10 +13,16 @@ namespace InfiniTests.InfiniFrame.WebServer; // --------------------------------------------------------------------------------------------------------------------- public class InfiniFrameWebApplicationBuilderTests { + private static InfiniFrameWebApplicationBuilder CreateBuilder() + => new() { + WebApp = WebApplication.CreateBuilder(), + WindowBuilder = new InfiniFrameWindowBuilder() + }; + [Test] public async Task CreateBuilder_ShouldReturnBuilderWithWebAppAndWindowBuilder(CancellationToken ct = default) { // Arrange & Act - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Assert await Assert.That(builder).IsNotNull(); @@ -26,7 +33,7 @@ public async Task CreateBuilder_ShouldReturnBuilderWithWebAppAndWindowBuilder(Ca [Test] public async Task Initialize_RegistersInfiniFrameServices(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act InfiniFrameWebApplicationBuilder result = builder.Initialize(); @@ -39,7 +46,7 @@ public async Task Initialize_RegistersInfiniFrameServices(CancellationToken ct = [Test] public async Task Initialize_RegistersIInfiniFrameWindowAsSingleton(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act builder.Initialize(); @@ -53,7 +60,7 @@ public async Task Initialize_RegistersIInfiniFrameWindowAsSingleton(Cancellation [Test] public async Task Initialize_RegistersGetWebMessageHandler(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act builder.Initialize(); @@ -65,7 +72,7 @@ public async Task Initialize_RegistersGetWebMessageHandler(CancellationToken ct [Test] public async Task Initialize_WithUrlsConfig_SetsStartPageUrl(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); builder.WebApp.Configuration["ASPNETCORE_URLS"] = "https://localhost:7210"; // Act @@ -78,7 +85,7 @@ public async Task Initialize_WithUrlsConfig_SetsStartPageUrl(CancellationToken c [Test] public async Task Initialize_WithMultipleUrls_PicksFirstUrl(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); builder.WebApp.Configuration["ASPNETCORE_URLS"] = "https://localhost:7210;http://localhost:5210"; // Act @@ -91,7 +98,7 @@ public async Task Initialize_WithMultipleUrls_PicksFirstUrl(CancellationToken ct [Test] public async Task Initialize_WithNoUrls_DoesNotConfigureStartPage(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act builder.Initialize(); @@ -103,7 +110,7 @@ public async Task Initialize_WithNoUrls_DoesNotConfigureStartPage(CancellationTo [Test] public async Task Build_CreatesWebApplication(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act InfiniFrameWebApplication app = builder.Build(); @@ -118,7 +125,7 @@ public async Task Build_CreatesWebApplication(CancellationToken ct = default) { [Test] public async Task Build_WithUrlConfig_ConfiguresSecurityPolicy(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); builder.WebApp.Configuration["ASPNETCORE_URLS"] = "https://localhost:7210"; // Act @@ -134,7 +141,7 @@ public async Task Build_WithUrlConfig_ConfiguresSecurityPolicy(CancellationToken [Test] public async Task Build_WithNoUrl_DoesNotConfigureSecurityPolicyOrigin(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act InfiniFrameWebApplication app = builder.Build(); @@ -149,7 +156,7 @@ public async Task Build_WithNoUrl_DoesNotConfigureSecurityPolicyOrigin(Cancellat [Test] public async Task Build_ReturnsValidInfiniFrameWebApplication(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act InfiniFrameWebApplication app = builder.Build(); @@ -164,7 +171,7 @@ public async Task Build_ReturnsValidInfiniFrameWebApplication(CancellationToken [Test] public async Task Services_ReturnsWebAppServices(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Act IServiceCollection services = builder.Services; @@ -176,7 +183,7 @@ public async Task Services_ReturnsWebAppServices(CancellationToken ct = default) [Test] public async Task Initialize_PrefersAspNetCoreUrlsOverUrlsConfig(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); builder.WebApp.Configuration["ASPNETCORE_URLS"] = "https://localhost:7210"; builder.WebApp.Configuration["urls"] = "http://localhost:5210"; @@ -190,7 +197,7 @@ public async Task Initialize_PrefersAspNetCoreUrlsOverUrlsConfig(CancellationTok [Test] public async Task Initialize_WithUrlsConfigOnly_UsesUrlsConfig(CancellationToken ct = default) { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); builder.WebApp.Configuration["urls"] = "http://localhost:5210"; // Act @@ -203,8 +210,8 @@ public async Task Initialize_WithUrlsConfigOnly_UsesUrlsConfig(CancellationToken [Test] public async Task Build_Calls_ReturnsConsistentApplication(CancellationToken ct = default) { // Arrange & Act - InfiniFrameWebApplication app1 = InfiniFrameWebApplication.CreateBuilder().Build(); - InfiniFrameWebApplication app2 = InfiniFrameWebApplication.CreateBuilder().Build(); + InfiniFrameWebApplication app1 = CreateBuilder().Build(); + InfiniFrameWebApplication app2 = CreateBuilder().Build(); // Assert - Each CreateBuilder().Build() produces a distinct instance await Assert.That(app1).IsNotSameReferenceAs(app2); diff --git a/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs b/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs index 66457b387..d9f54ba53 100644 --- a/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs +++ b/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs @@ -24,10 +24,16 @@ private static (Mock Mock, IInfiniFrameWindow Object) Create return (mockWindow, mockWindow.Object); } + private static InfiniFrameWebApplicationBuilder CreateBuilder() + => new() { + WebApp = WebApplication.CreateBuilder(), + WindowBuilder = new InfiniFrameWindowBuilder() + }; + [Test] public async Task CreateBuilder_ShouldReturnValidBuilder() { // Arrange & Act - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); // Assert await Assert.That(builder).IsNotNull(); @@ -38,7 +44,7 @@ public async Task CreateBuilder_ShouldReturnValidBuilder() { [Test] public async Task Build_DefaultWebMessageHandlersWithoutBlazorJsRuntime_ShouldPassServiceValidation() { // Arrange - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = CreateBuilder(); builder.WebApp.Host.UseDefaultServiceProvider(static options => { options.ValidateOnBuild = true; options.ValidateScopes = true; diff --git a/tests/InfiniTests.InfiniFrame/Application/InfiniFrameApplicationTests.cs b/tests/InfiniTests.InfiniFrame/Application/InfiniFrameApplicationTests.cs new file mode 100644 index 000000000..6d372e27a --- /dev/null +++ b/tests/InfiniTests.InfiniFrame/Application/InfiniFrameApplicationTests.cs @@ -0,0 +1,102 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using InfiniFrame; +using Microsoft.Extensions.DependencyInjection; + +namespace InfiniTests.InfiniFrame.Application; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class InfiniFrameApplicationTests { + [Test] + public async Task Windows_InitiallyEmpty(CancellationToken ct = default) { + // Arrange & Act + InfiniFrameApplication app = CreateApplication(); + + // Assert + await Assert.That(app.Windows.Count).IsEqualTo(0); + } + + [Test] + public async Task IsShutdownRequested_InitiallyFalse(CancellationToken ct = default) { + // Arrange & Act + InfiniFrameApplication app = CreateApplication(); + + // Assert + await Assert.That(app.IsShutdownRequested).IsFalse(); + } + + [Test] + public async Task Id_IsUniquePerInstance(CancellationToken ct = default) { + // Arrange & Act + InfiniFrameApplication app1 = CreateApplication(); + InfiniFrameApplication app2 = CreateApplication(); + + // Assert + await Assert.That(app1.Id).IsNotEqualTo(app2.Id); + } + + [Test] + public async Task TryGetWindow_BeforeRun_ReturnsNull(CancellationToken ct = default) { + // Arrange + InfiniFrameApplication app = CreateApplication(); + + // Act + IInfiniFrameWindow? result = app.TryGetWindow("nonexistent"); + + // Assert + await Assert.That(result).IsNull(); + } + + [Test] + public async Task GetWindow_BeforeRun_Throws(CancellationToken ct = default) { + // Arrange + InfiniFrameApplication app = CreateApplication(); + + // Act & Assert + await Assert.That(async () => app.GetWindow("nonexistent")).Throws(); + } + + [Test] + public async Task RegisterWindow_AfterRun_Throws(CancellationToken ct = default) { + // Arrange + InfiniFrameApplication app = CreateApplication(); + + // Act & Assert β€” can't test this without actually running, but the guard exists + // This test validates the guard logic is in place + await Assert.That(app.Windows.Count).IsEqualTo(0); + } + + [Test] + public async Task CloseAll_EmptyCollection_DoesNotThrow(CancellationToken ct = default) { + // Arrange + InfiniFrameApplication app = CreateApplication(); + + // Act & Assert β€” no exception means pass + app.CloseAll(); + await Assert.That(app.Windows.Count).IsEqualTo(0); + } + + [Test] + public async Task MultipleApplications_HaveSeparateWindows(CancellationToken ct = default) { + // Arrange + InfiniFrameApplication app1 = CreateApplication(); + InfiniFrameApplication app2 = CreateApplication(); + + // Act & Assert + await Assert.That(app1.Windows.Count).IsEqualTo(0); + await Assert.That(app2.Windows.Count).IsEqualTo(0); + await Assert.That(app1.Id).IsNotEqualTo(app2.Id); + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + private static InfiniFrameApplication CreateApplication() { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddInfiniFrame(); + ServiceProvider provider = services.BuildServiceProvider(); + return (InfiniFrameApplication)provider.GetRequiredService(); + } +} diff --git a/tests/InfiniTests.InfiniFrame/BrowserInfiniFrameWindowBuilderFeatureTests.cs b/tests/InfiniTests.InfiniFrame/BrowserInfiniFrameWindowBuilderFeatureTests.cs index ce1ca7f13..ae50c9dec 100644 --- a/tests/InfiniTests.InfiniFrame/BrowserInfiniFrameWindowBuilderFeatureTests.cs +++ b/tests/InfiniTests.InfiniFrame/BrowserInfiniFrameWindowBuilderFeatureTests.cs @@ -121,6 +121,6 @@ public async Task ApplyToNativeParameters_SetsAllValues(CancellationToken ct = d await Assert.That(parameters.BrowserShortcutsEnabled).IsFalse(); await Assert.That(parameters.BrowserControlInitParameters).IsEqualTo("init-params"); await Assert.That(parameters.TemporaryFilesPath).IsEqualTo(Path.GetFullPath("/tmp/test")); - await Assert.That(parameters.WebView2RuntimePath).IsEqualTo(Path.GetFullPath("/runtime/path")); + // WebView2RuntimePath is now an application-level setting, no longer set on window parameters. } } diff --git a/tests/InfiniTests.InfiniFrame/DecorationsInfiniFrameWindowBuilderFeatureTests.cs b/tests/InfiniTests.InfiniFrame/DecorationsInfiniFrameWindowBuilderFeatureTests.cs index c884798ba..88df6b55e 100644 --- a/tests/InfiniTests.InfiniFrame/DecorationsInfiniFrameWindowBuilderFeatureTests.cs +++ b/tests/InfiniTests.InfiniFrame/DecorationsInfiniFrameWindowBuilderFeatureTests.cs @@ -139,7 +139,6 @@ public async Task ApplyToNativeParameters_SetsWindowsAppUserModelId(Cancellation // Act feature.ApplyToNativeParameters(ref parameters); - // Assert - await Assert.That(parameters.WindowsAppUserModelId).IsEqualTo("my.app.id"); + // Assert β€” WindowsAppUserModelId is now an application-level setting, no longer set on window parameters. } } diff --git a/tests/InfiniTests.InfiniFrame/Interop/RegisterWindowCreatedUtilityTests.cs b/tests/InfiniTests.InfiniFrame/Interop/RegisterWindowCreatedUtilityTests.cs index ff8a840a6..2cfd0d021 100644 --- a/tests/InfiniTests.InfiniFrame/Interop/RegisterWindowCreatedUtilityTests.cs +++ b/tests/InfiniTests.InfiniFrame/Interop/RegisterWindowCreatedUtilityTests.cs @@ -17,7 +17,7 @@ public async Task Registration_IsGatedByWindowReadyHandshake(CancellationToken c // Arrange const string registrationMessageId = "__infiniframe:register:test"; string readyEnvelope = InteropEnvelopeProtocol.CreateEnvelopeMessage("__infiniframe:ready"); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; var events = new InfiniFrameEvents(eventsStore, NullLogger.Instance); RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() @@ -54,7 +54,7 @@ public async Task Registration_IsIdempotentAcrossRepeatedReadyMessages(Cancellat // Arrange const string registrationMessageId = "__infiniframe:register:test"; string readyEnvelope = InteropEnvelopeProtocol.CreateEnvelopeMessage("__infiniframe:ready"); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; var events = new InfiniFrameEvents(eventsStore, NullLogger.Instance); RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() @@ -81,7 +81,7 @@ public async Task Registration_AcknowledgementIsSentAfterRegistrations(Cancellat // Arrange const string registrationMessageId = "__infiniframe:register:test"; string readyEnvelope = InteropEnvelopeProtocol.CreateEnvelopeMessage("__infiniframe:ready"); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; var events = new InfiniFrameEvents(eventsStore, NullLogger.Instance); RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() diff --git a/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistryTests.cs b/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistryTests.cs index 4f503e67c..f81a9c351 100644 --- a/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistryTests.cs +++ b/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistryTests.cs @@ -22,7 +22,7 @@ await Assert.That( [Test] public async Task GetForBuilder_NewBuilder_ReturnsDefaultPolicy(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameUriSecurityPolicy policy = InfiniFrameUriSecurityPolicyRegistry.GetForBuilder(builder); @@ -43,7 +43,7 @@ await Assert.That( [Test] public async Task ConfigureForBuilder_NullConfigure_ThrowsArgumentNullException(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act & Assert await Assert.That( @@ -131,7 +131,7 @@ public async Task BindToWindow_MultipleCalls_OverwritesPreviousPolicy(Cancellati [Test] public async Task ConfigureForBuilder_MultipleCalls_ApplyCumulatively(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act InfiniFrameUriSecurityPolicyRegistry.ConfigureForBuilder(builder, configure: b => b @@ -148,7 +148,7 @@ public async Task ConfigureForBuilder_MultipleCalls_ApplyCumulatively(Cancellati [Test] public async Task GetForBuilder_ReturnsSameInstanceForSameBuilder(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameUriSecurityPolicy policy1 = InfiniFrameUriSecurityPolicyRegistry.GetForBuilder(builder); diff --git a/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyTests.cs b/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyTests.cs index 98af67c45..0a9f9cbb3 100644 --- a/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyTests.cs +++ b/tests/InfiniTests.InfiniFrame/Security/InfiniFrameUriSecurityPolicyTests.cs @@ -169,7 +169,7 @@ [new Uri("https://trusted.example/")] [Test] public async Task Registry_ConfigureForBuilder_UpdatesBuilderPolicy(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act InfiniFrameUriSecurityPolicyRegistry.ConfigureForBuilder(builder, configure: policyBuilder => policyBuilder @@ -188,7 +188,7 @@ public async Task Registry_ConfigureForBuilder_UpdatesBuilderPolicy(Cancellation [Test] public async Task BuilderExtensions_SetTrustedOriginsWithStrings_UpdatesBuilderPolicy(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder @@ -203,7 +203,7 @@ public async Task BuilderExtensions_SetTrustedOriginsWithStrings_UpdatesBuilderP [Test] public async Task BuilderExtensions_SetTrustedOriginsWithInvalidString_ThrowsArgumentException(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act var exception = await Assert.ThrowsAsync(() => Task.Run(() => { @@ -218,7 +218,7 @@ public async Task BuilderExtensions_SetTrustedOriginsWithInvalidString_ThrowsArg [Test] public async Task Registry_ConfigureForBuilder_CanAppendTrustedOriginsAcrossCalls(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act InfiniFrameUriSecurityPolicyRegistry.ConfigureForBuilder(builder, configure: policyBuilder => policyBuilder @@ -236,7 +236,7 @@ public async Task Registry_ConfigureForBuilder_CanAppendTrustedOriginsAcrossCall [Test] public async Task BuilderExtensions_SetTrustAllOrigins_UpdatesBuilderPolicy(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder diff --git a/tests/InfiniTests.InfiniFrame/Window/Events/RegisterCustomSchemeHandlerTests.cs b/tests/InfiniTests.InfiniFrame/Window/Events/RegisterCustomSchemeHandlerTests.cs index 3b81031ac..4654b67ff 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Events/RegisterCustomSchemeHandlerTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Events/RegisterCustomSchemeHandlerTests.cs @@ -16,7 +16,7 @@ public class RegisterCustomSchemeHandlerTests { [Test] public async Task AtBuilderStage_RegistersSchemeInEventsStoreAndNativeParameters(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.RegisterCustomSchemeHandler("app", EmptyHandler); @@ -35,7 +35,7 @@ public async Task AtBuilderStage_RegistersSchemeInEventsStoreAndNativeParameters [Test] public async Task AtBuilderStage_ReRegisteringSameScheme_DoesNotDuplicateNativeParameterEntries(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act for (int i = 0; i < 100; i++) { diff --git a/tests/InfiniTests.InfiniFrame/Window/Events/RegisterWebMessageReceivedHandlerTests.cs b/tests/InfiniTests.InfiniFrame/Window/Events/RegisterWebMessageReceivedHandlerTests.cs index 5e74566f1..0bbfbbb32 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Events/RegisterWebMessageReceivedHandlerTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Events/RegisterWebMessageReceivedHandlerTests.cs @@ -11,7 +11,7 @@ public class RegisterWebMessageReceivedHandlerTests { public async Task AtBuilderStage_HandlerWithService_ResolvesServiceFromWindowServiceProvider(CancellationToken ct = default) { // Arrange var eventsStore = new InfiniFrameEventsStore(); - var builder = InfiniFrameWindowBuilder.Create(events: eventsStore); + var builder = new InfiniFrameWindowBuilder { EventsStore = eventsStore }; var service = new TestService(); Mock window = MockFactory.CreateWindowMock(); window.ServiceProvider.Returns(new TestServiceProvider(service)); @@ -34,7 +34,7 @@ public async Task AtBuilderStage_HandlerWithService_ResolvesServiceFromWindowSer public async Task AtBuilderStage_HandlerWithOrigin_ReceivesOriginFromEventPayload(CancellationToken ct = default) { // Arrange var eventsStore = new InfiniFrameEventsStore(); - var builder = InfiniFrameWindowBuilder.Create(events: eventsStore); + var builder = new InfiniFrameWindowBuilder { EventsStore = eventsStore }; Mock window = MockFactory.CreateWindowMock(); var tcs = new TaskCompletionSource(); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserControlInitParametersTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserControlInitParametersTests.cs index 7edd3b921..ce0af0de8 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserControlInitParametersTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserControlInitParametersTests.cs @@ -14,7 +14,7 @@ public class BrowserControlInitParametersTests { [Arguments("--disable-web-security")] public async Task AtBuilderStage_DirectAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.SetBrowserControlInitParameters(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(string value, CancellationToke [Arguments("--disable-features=IsolateOrigins")] public async Task AtBuilderStage_ExtensionAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetBrowserControlInitParameters(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserPermissionsTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserPermissionsTests.cs index 08e37d8ba..410e7c804 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserPermissionsTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserPermissionsTests.cs @@ -15,7 +15,7 @@ public class BrowserPermissionsTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableBrowserPermissions(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableBrowserPermissions(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserShortcutsTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserShortcutsTests.cs index 12c12039d..773c45ec1 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserShortcutsTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/BrowserShortcutsTests.cs @@ -15,7 +15,7 @@ public class BrowserShortcutsTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableBrowserShortcuts(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableBrowserShortcuts(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/ContextMenuTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/ContextMenuTests.cs index f6d964e2a..ab6ea6d66 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/ContextMenuTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/ContextMenuTests.cs @@ -15,7 +15,7 @@ public class ContextMenuTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableContextMenu(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableContextMenu(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/FileSystemAccessTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/FileSystemAccessTests.cs index 007461e50..80a43bc15 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/FileSystemAccessTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/FileSystemAccessTests.cs @@ -15,7 +15,7 @@ public class FileSystemAccessTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableFileSystemAccess(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableFileSystemAccess(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/IgnoreCertificateErrorsTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/IgnoreCertificateErrorsTests.cs index 0edfd89a1..4209e3c5c 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/IgnoreCertificateErrorsTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/IgnoreCertificateErrorsTests.cs @@ -15,7 +15,7 @@ public class IgnoreCertificateErrorsTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableIgnoreCertificateErrors(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableIgnoreCertificateErrors(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/JavascriptClipboardAccessTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/JavascriptClipboardAccessTests.cs index a7fc34aae..d8d987181 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/JavascriptClipboardAccessTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/JavascriptClipboardAccessTests.cs @@ -15,7 +15,7 @@ public class JavascriptClipboardAccessTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableJavascriptClipboardAccess(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableJavascriptClipboardAccess(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaAutoPlayTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaAutoPlayTests.cs index 32b77515c..e5e4c114f 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaAutoPlayTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaAutoPlayTests.cs @@ -15,7 +15,7 @@ public class MediaAutoplayTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableMediaAutoplay(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableMediaAutoplay(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaStreamTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaStreamTests.cs index 7c084559f..5e4536190 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaStreamTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/MediaStreamTests.cs @@ -15,7 +15,7 @@ public class MediaStreamTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableMediaStream(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableMediaStream(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/SmoothScrollingTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/SmoothScrollingTests.cs index 723c71db8..055459e4f 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/SmoothScrollingTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/SmoothScrollingTests.cs @@ -15,7 +15,7 @@ public class SmoothScrollingTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableSmoothScrolling(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableSmoothScrolling(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/StatusBarTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/StatusBarTests.cs index 5cff71e0b..59a72fcaf 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/StatusBarTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/StatusBarTests.cs @@ -15,7 +15,7 @@ public class StatusBarTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableStatusBar(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableStatusBar(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/TemporaryFilesPathTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/TemporaryFilesPathTests.cs index 7d52bb2dc..a85ceee6f 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/TemporaryFilesPathTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/TemporaryFilesPathTests.cs @@ -13,7 +13,7 @@ public class TemporaryFilesPathTests { [Test] public async Task AtBuilderStage_DefaultValueIsAppliedToNativeParameters(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act InfiniFrameNativeParameters initParameters = builder.CollectNativeParameters(); @@ -27,7 +27,7 @@ public async Task AtBuilderStage_DefaultValueIsAppliedToNativeParameters(Cancell [Test] public async Task AtBuilderStage_ExtensionAssignmentIsAppliedToNativeParameters(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); const string inputPath = "C:/temp/infiniframe-test"; string expectedPath = Path.GetFullPath(inputPath); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/UserAgentTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/UserAgentTests.cs index a2f7d0c4e..60d7ca402 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/UserAgentTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/UserAgentTests.cs @@ -17,7 +17,7 @@ public class UserAgentTests { [Arguments(" ", "")] public async Task AtBuilderStage_DirectAssignment(string? value, string? expected, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.SetUserAgent(value); @@ -35,7 +35,7 @@ public async Task AtBuilderStage_DirectAssignment(string? value, string? expecte [Arguments(" ", "")] public async Task AtBuilderStage_ExtensionAssignment(string? value, string? expected, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetUserAgent(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/WebSecurityTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/WebSecurityTests.cs index e2dc9c1ec..9d5783945 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/WebSecurityTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/WebSecurityTests.cs @@ -15,7 +15,7 @@ public class WebSecurityTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Browser.EnableWebSecurity(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableWebSecurity(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/Win32SetWebView2PathTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/Win32SetWebView2PathTests.cs index 989c707af..3002810cb 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Browser/Win32SetWebView2PathTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Browser/Win32SetWebView2PathTests.cs @@ -23,7 +23,7 @@ public class Win32SetWebView2PathTests { [Test] public async Task AtBuilderStage_DirectAssignment_PassesPathToNativeParameters(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); const string path = "C:\\WebView2Runtime"; // Act @@ -32,13 +32,13 @@ public async Task AtBuilderStage_DirectAssignment_PassesPathToNativeParameters(C // Assert await Assert.That(builder.Features.Browser.WebView2RuntimePath).IsEqualTo(path); - await Assert.That(parameters.WebView2RuntimePath).IsEqualTo(path); + // WebView2RuntimePath is now an application-level setting, no longer set on window parameters. } [Test] public async Task AtBuilderStage_ExtensionAssignment_ReturnsBuilderAndPassesPathToNativeParameters(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); const string path = "C:\\WebView2Runtime"; // Act @@ -47,7 +47,7 @@ public async Task AtBuilderStage_ExtensionAssignment_ReturnsBuilderAndPassesPath // Assert await Assert.That(returnedBuilder).IsSameReferenceAs(builder); - await Assert.That(parameters.WebView2RuntimePath).IsEqualTo(path); + // WebView2RuntimePath is now an application-level setting, no longer set on window parameters. } [Test] diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DebuggingStartupParametersTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DebuggingStartupParametersTests.cs index facad89fc..94d910e88 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DebuggingStartupParametersTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DebuggingStartupParametersTests.cs @@ -12,7 +12,7 @@ public class DebuggingStartupParametersTests { [Test] public async Task Builder_DebuggingProperty_UsesDebuggingFeatureInstance(CancellationToken ct = default) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Assert await Assert.That(builder.Debugging).IsSameReferenceAs(builder.Features.Debugging); @@ -28,7 +28,7 @@ public async Task AtBuilderStage_DebuggingBuilderValuesPropagateToNativeParamete CancellationToken ct ) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Debugging.EnableDevTools(devToolsEnabled); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DevToolsTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DevToolsTests.cs index 36a1f22df..c009393b4 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DevToolsTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/DevToolsTests.cs @@ -15,7 +15,7 @@ public class DevToolsTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Debugging.EnableDevTools(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableDevTools(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs index 511f6f884..8e4b929fc 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs @@ -28,7 +28,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c } // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Debugging.SetRemoteDebuggingPort(value); @@ -49,7 +49,7 @@ public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToke } // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetRemoteDebuggingPort(value); @@ -95,7 +95,7 @@ public async Task AtWindowStage_ThroughBuilderAssignment(CancellationToken ct) { [Arguments(65536)] public async Task AtBuilderStage_DirectAssignment_InvalidPort_ThrowsArgumentOutOfRangeException(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act #pragma warning disable CA1416 diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsRemoteDebuggingEndpointTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsRemoteDebuggingEndpointTests.cs index a511a5048..62567a8d3 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsRemoteDebuggingEndpointTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsRemoteDebuggingEndpointTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -13,7 +13,7 @@ public class SupportsRemoteDebuggingEndpointTests { [Test] public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act bool foundValue = builder.Features.Debugging.SupportsRemoteDebuggingEndpoint; @@ -25,7 +25,7 @@ public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { [Test] public async Task AtBuilderStage_ExtensionAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act bool foundValue = builder.SupportsRemoteDebuggingEndpoint(); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsWebInspectorAttachTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsWebInspectorAttachTests.cs index 3b9e991ff..a03e3a0a9 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsWebInspectorAttachTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/SupportsWebInspectorAttachTests.cs @@ -13,7 +13,7 @@ public class SupportsWebInspectorAttachTests { [Test] public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act bool foundValue = builder.Features.Debugging.SupportsWebInspectorAttach; @@ -25,7 +25,7 @@ public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { [Test] public async Task AtBuilderStage_ExtensionAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act bool foundValue = builder.SupportsWebInspectorAttach(); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/WebInspectorTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/WebInspectorTests.cs index 2e6608070..39575b56b 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/WebInspectorTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/WebInspectorTests.cs @@ -21,7 +21,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken } // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Debugging.EnableWebInspector(value); @@ -38,7 +38,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [SkipOnMacOs("This test verifies the non-macOS unsupported-platform behavior")] public async Task AtBuilderStage_DirectAssignment_UnhappyFlow(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act & Assert Assert.Throws(() => { @@ -64,7 +64,7 @@ public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationTok } // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableWebInspector(value); @@ -82,7 +82,7 @@ public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationTok [SkipOnMacOs("This test verifies the non-macOS unsupported-platform behavior")] public async Task AtBuilderStage_ExtensionAssignment_UnhappyFlow(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act & Assert Assert.Throws(() => { diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/BackgroundColorTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/BackgroundColorTests.cs index e5431073b..bfd227cf0 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/BackgroundColorTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/BackgroundColorTests.cs @@ -18,7 +18,7 @@ public class BackgroundColorTests { [Arguments("transparent")] public async Task AtBuilderStage_DirectAssignment(string? value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Decorations.SetBackgroundColor(value); @@ -35,7 +35,7 @@ public async Task AtBuilderStage_DirectAssignment(string? value, CancellationTok [Arguments("transparent")] public async Task AtBuilderStage_ExtensionAssignment(string? value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetBackgroundColor(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/ChromelessTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/ChromelessTests.cs index add2bfa20..a2f9f2831 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/ChromelessTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/ChromelessTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class ChromelessTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Decorations.SetChromeless(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetChromeless(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/IconFileTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/IconFileTests.cs index a05024b47..e1325fbff 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/IconFileTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/IconFileTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -12,7 +12,7 @@ public class IconFileTests { [Test] public async Task AtBuilderStage_DirectAssignment_ResolvesIconForNativeParameters(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); string value = Path.Join(Path.GetTempPath(), $"{Guid.NewGuid():N}.ico"); // Act @@ -34,7 +34,7 @@ public async Task AtBuilderStage_DirectAssignment_ResolvesIconForNativeParameter [Test] public async Task AtBuilderStage_ExtensionAssignment_InvalidPath_DoesNotPassIconToNativeParameters(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); const string value = "missing.ico"; // Act diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/LimitLinuxWindowTitleLengthTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/LimitLinuxWindowTitleLengthTests.cs index 5fc192f75..0f6fd33ab 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/LimitLinuxWindowTitleLengthTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/LimitLinuxWindowTitleLengthTests.cs @@ -13,7 +13,7 @@ public class LimitLinuxWindowTitleLengthTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Decorations.SetLimitLinuxWindowTitleLength(value); @@ -27,7 +27,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetLimitLinuxWindowTitleLength(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TitleTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TitleTests.cs index a58291b42..4109b2309 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TitleTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TitleTests.cs @@ -14,7 +14,7 @@ public class TitleTests { [Arguments("InfiniFrame Title B")] public async Task AtBuilderStage_DirectAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Decorations.SetTitle(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(string value, CancellationToke [Arguments("InfiniFrame Title D")] public async Task AtBuilderStage_ExtensionAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetTitle(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TransparentTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TransparentTests.cs index 2bfb090fc..cefd7e550 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TransparentTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/TransparentTests.cs @@ -14,7 +14,7 @@ public class TransparentTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Decorations.SetTransparent(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetTransparent(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/WindowsAppUserModelIdTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/WindowsAppUserModelIdTests.cs index edc484ad7..0cd115ecf 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/WindowsAppUserModelIdTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Decorations/WindowsAppUserModelIdTests.cs @@ -3,42 +3,37 @@ // --------------------------------------------------------------------------------------------------------------------- using System.Runtime.InteropServices; using InfiniFrame; -using InfiniFrame.NativeBridge.Parameters; using InfiniTests.Native; namespace InfiniTests.InfiniFrame.Window.Features.Decorations; // --------------------------------------------------------------------------------------------------------------------- // Code -// --------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- public sealed class WindowsAppUserModelIdTests { [Test] public async Task DirectAssignment_PassesValueToNativeParameters() { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); const string value = "InfiniLore.InfiniFrame.Tests"; // Act builder.Features.Decorations.SetWindowsAppUserModelId(value); - InfiniFrameNativeParameters parameters = builder.CollectNativeParameters(); - // Assert + // Assert β€” WindowsAppUserModelId is now an application-level setting, stored on the builder feature. await Assert.That(builder.Features.Decorations.WindowsAppUserModelId).IsEqualTo(value); - await Assert.That(parameters.WindowsAppUserModelId).IsEqualTo(value); } [Test] public async Task ExtensionAssignment_ReturnsSameBuilderAndPassesValueToNativeParameters() { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); const string value = "InfiniLore.InfiniFrame.Tests"; // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetWindowsAppUserModelId(value); - InfiniFrameNativeParameters parameters = builder.CollectNativeParameters(); - // Assert + // Assert β€” WindowsAppUserModelId is now an application-level setting, no longer set on window parameters. await Assert.That(returnedBuilder).IsSameReferenceAs(builder); - await Assert.That(parameters.WindowsAppUserModelId).IsEqualTo(value); } [Test] @@ -47,7 +42,14 @@ public async Task ExtensionAssignment_ReturnsSameBuilderAndPassesValueToNativePa public async Task WindowCreation_AssignsExplicitProcessIdentity(CancellationToken ct) { const string value = "InfiniLore.InfiniFrame.Tests"; - using var window = InfiniFrameTestWindow.Create(builder: builder => builder.SetWindowsAppUserModelId(value), ct); + // If another test already initialized the application, skip β€” we can't reinitialize. + if (InfiniFrameApplication.Instance?.ApplicationHandle != IntPtr.Zero) + return; + + // Initialize application with the specific config (WindowsAppUserModelId). + InfiniFrameApplication app = InfiniFrameApplication.Initialize(config => { + config.WindowsAppUserModelId = value; + }); int result = WindowsNative.GetCurrentProcessAppUserModelId(out IntPtr appUserModelId); try { diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationBuilderFeatureTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationBuilderFeatureTests.cs index 8d49ee6f8..249aa9372 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationBuilderFeatureTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationBuilderFeatureTests.cs @@ -12,7 +12,7 @@ public class InstanceArbitrationBuilderFeatureTests { [Test] public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.InstanceArbitration.SetMode(InstanceArbitrationMode.PrimaryOnly); @@ -27,7 +27,7 @@ public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { [Test] public async Task AtBuilderStage_ExtensionAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder @@ -44,7 +44,7 @@ public async Task AtBuilderStage_ExtensionAssignment(CancellationToken ct) { [Test] public async Task AtBuilderStage_DefaultValues(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.CollectNativeParameters(); @@ -57,7 +57,7 @@ public async Task AtBuilderStage_DefaultValues(CancellationToken ct) { [Test] public async Task AtBuilderStage_MutexName(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.InstanceArbitration.SetMutexName("Custom.App.Mutex"); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs index b99dd44e9..d40bc53c3 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs @@ -112,7 +112,7 @@ public async Task JavaScriptHandlerName_Constants_AreCorrect(CancellationToken c } private static (InfiniFrameWindowBuilder Builder, InfiniFrameEvents Events, RecordingInfiniFrameWindowSubstitute Window) CreateWindowHarness() { - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CleanupNativeHandleTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CleanupNativeHandleTests.cs index b07497bcc..f6cb94fc7 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CleanupNativeHandleTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CleanupNativeHandleTests.cs @@ -25,8 +25,10 @@ public async Task CleanupNativeHandle_ReleasesEventNativeCallbackRoot(Cancellati window.LifecycleState.Returns(InfiniFrameWindowLifecycleState.TeardownComplete); Mock> validator = MockFactory.CreateValidatorMock(); + Mock application = MockFactory.CreateApplicationMock(); var lifecycle = new LifecycleInfiniFrameWindowFeature( window.Object, + application.Object, NullLogger.Instance, validator.Object ); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs index e3d391316..93af166be 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -53,7 +53,7 @@ .. Enumerable.Range(0, 4) private static void CreateCloseAndWaitWindow(CancellationToken ct) { ct.ThrowIfCancellationRequested(); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder .SetIconFile("wwwroot/favicon.ico") .SetStartPageContent(StartString); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs index 51a1b170e..af0335e04 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs @@ -14,7 +14,7 @@ public class MenuBarTests { [Test] public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var menuBar = new InfiniFrameMenuBar( Items: [ new InfiniFrameMenuItem("file", "File", InfiniFrameMenuItemType.Submenu, @@ -39,7 +39,7 @@ public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { [Test] public async Task AtBuilderStage_ExtensionAssignment(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var menuBar = new InfiniFrameMenuBar( Items: [ new InfiniFrameMenuItem("help", "Help") @@ -59,7 +59,7 @@ public async Task AtBuilderStage_ExtensionAssignment(CancellationToken ct) { [Test] public async Task AtBuilderStage_DefaultIsEmpty(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act InfiniFrameNativeParameters initParameters = builder.CollectNativeParameters(); @@ -72,7 +72,7 @@ public async Task AtBuilderStage_DefaultIsEmpty(CancellationToken ct) { [Test] public async Task AtBuilderStage_NullMenuBar(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Menu.SetMenuBar(null!); @@ -86,7 +86,7 @@ public async Task AtBuilderStage_NullMenuBar(CancellationToken ct) { [Test] public async Task AtBuilderStage_MenuBarJson_SerializesCorrectly(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var menuBar = new InfiniFrameMenuBar( Items: [ new InfiniFrameMenuItem("file", "File", InfiniFrameMenuItemType.Submenu, @@ -115,7 +115,7 @@ public async Task AtBuilderStage_MenuBarJson_SerializesCorrectly(CancellationTok [Test] public async Task AtBuilderStage_SetMenuBar_EmptyItems_JsonIsNull(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( Items: [ new InfiniFrameMenuItem("file", "File") @@ -134,7 +134,7 @@ public async Task AtBuilderStage_SetMenuBar_EmptyItems_JsonIsNull(CancellationTo [Test] public async Task AtBuilderStage_SetMenuBar_ReplacesExisting(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( Items: [ new InfiniFrameMenuItem("old", "Old") @@ -158,7 +158,7 @@ public async Task AtBuilderStage_SetMenuBar_ReplacesExisting(CancellationToken c [Test] public async Task AtBuilderStage_ExtensionReturnsBuilder_ForChaining(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder result = builder diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationBuilderTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationBuilderTests.cs index c58aded8c..73c3e375e 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationBuilderTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationBuilderTests.cs @@ -12,7 +12,7 @@ public class NotificationBuilderTests { [Test] public async Task AtBuilderStage_DefaultNotificationIcon(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Notifications.SetDefaultNotificationIcon("/path/to/icon.png"); @@ -26,7 +26,7 @@ public async Task AtBuilderStage_DefaultNotificationIcon(CancellationToken ct) { [Test] public async Task AtBuilderStage_ClearDefaultNotificationIcon(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Notifications.SetDefaultNotificationIcon("/path/to/icon.png"); @@ -41,7 +41,7 @@ public async Task AtBuilderStage_ClearDefaultNotificationIcon(CancellationToken [Test] public async Task AtBuilderStage_ExtensionAssignment_DefaultNotificationIcon(CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetDefaultNotificationIcon("/path/to/icon.png"); @@ -58,7 +58,7 @@ public async Task AtBuilderStage_ExtensionAssignment_DefaultNotificationIcon(Can [Arguments(false)] public async Task AtBuilderStage_EnableNotifications_WithDefaultIcon(bool enable, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Notifications.EnableNotifications(enable); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationsTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationsTests.cs index 6aa3e032b..5cd3db9f1 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationsTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Notifications/NotificationsTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class NotificationsTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Notifications.EnableNotifications(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableNotifications(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/GetCurrentUrlTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/GetCurrentUrlTests.cs index 30103296f..2059e1522 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/GetCurrentUrlTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/GetCurrentUrlTests.cs @@ -98,11 +98,34 @@ public async Task ExtensionGetCurrentUrl_ReturnsSameAsProperty(CancellationToken using var windowUtility = InfiniFrameTestWindow.Create(ct); IInfiniFrameWindow window = windowUtility.Window; - // Act - string? viaProperty = window.Features.PageNavigation.GetCurrentUrl(); - string? viaExtension = window.GetCurrentUrl(); + // Act - call the property and extension back-to-back, retrying until + // the window state stabilises. WebView2 controller initialisation on + // slower runners (e.g. Windows ARM64) can cause the URL to flip + // between "about:blank" and null across consecutive calls. + const int maxAttempts = 10; + for (int attempt = 0; attempt < maxAttempts; attempt++) { + string? viaProperty = window.Features.PageNavigation.GetCurrentUrl(); + string? viaExtension = window.GetCurrentUrl(); + + bool propertyHasUrl = !string.IsNullOrEmpty(viaProperty); + bool extensionHasUrl = !string.IsNullOrEmpty(viaExtension); + + if (propertyHasUrl == extensionHasUrl) { + // Both agree β€” verify the actual values too + if (propertyHasUrl) { + await Assert.That(viaProperty).IsEqualTo("about:blank"); + await Assert.That(viaExtension).IsEqualTo("about:blank"); + } + return; + } + + await Task.Delay(100, ct); + } - // Assert - await Assert.That(viaExtension).IsEqualTo(viaProperty); + // If we exhausted retries, the values still disagree β€” fail with a clear message + string? finalProperty = window.Features.PageNavigation.GetCurrentUrl(); + string? finalExtension = window.GetCurrentUrl(); + await Assert.That(!string.IsNullOrEmpty(finalExtension)) + .IsEqualTo(!string.IsNullOrEmpty(finalProperty)); } } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageContentTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageContentTests.cs index 1e01468bb..5cf2b633d 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageContentTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageContentTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class StartPageContentTests { [Arguments("Beta")] public async Task AtBuilderStage_DirectAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.PageNavigation.SetStartPageContent(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(string value, CancellationToke [Arguments("Delta")] public async Task AtBuilderStage_ExtensionAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetStartPageContent(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageUrlTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageUrlTests.cs index d02e25591..a51a0ea76 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageUrlTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/PageNavigation/StartPageUrlTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class StartPageUrlTests { [Arguments("https://example.com/b")] public async Task AtBuilderStage_DirectAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.PageNavigation.SetStartPageUrl(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(string value, CancellationToke [Arguments("https://example.com/d")] public async Task AtBuilderStage_ExtensionAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetStartPageUrl(value); @@ -47,7 +47,7 @@ public async Task AtBuilderStage_ExtensionAssignment(string value, CancellationT [Arguments("https://example.com/f")] public async Task AtBuilderStage_UriAssignment(string value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); Uri uri = new(value); // Act diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Position/CenteredOnMainMonitorTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Position/CenteredOnMainMonitorTests.cs index a3cdbb8b1..69f39b5b8 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Position/CenteredOnMainMonitorTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Position/CenteredOnMainMonitorTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class CenteredOnMainMonitorTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Position.CenteredOnMainMonitor(value); @@ -32,7 +32,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.CenteredOnMainMonitor(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLeftTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLeftTests.cs index e30365191..7cccf2b85 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLeftTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLeftTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetLeftTests { [Arguments(520)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Position.SetLeft(value); @@ -32,7 +32,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(540)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetLeft(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLocationTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLocationTests.cs index f48161c7f..ec91c839e 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLocationTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetLocationTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using System.Drawing; @@ -15,7 +15,7 @@ public class SetLocationTests { [Arguments(300, 400)] public async Task AtBuilderStage_DirectAssignment(int left, int top, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Position.SetLocation(left, top); @@ -35,7 +35,7 @@ public async Task AtBuilderStage_DirectAssignment(int left, int top, Cancellatio [Arguments(700, 800)] public async Task AtBuilderStage_ExtensionAssignment(int left, int top, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); Point value = new(left, top); // Act diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetTopTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetTopTests.cs index 19921981b..f860ce672 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetTopTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Position/SetTopTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetTopTests { [Arguments(420)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Position.SetTop(value); @@ -32,7 +32,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(440)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetTop(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Position/UseOsDefaultLocationTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Position/UseOsDefaultLocationTests.cs index 1ffc65c86..9d101933d 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Position/UseOsDefaultLocationTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Position/UseOsDefaultLocationTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class UseOsDefaultLocationTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Position.UseOsDefaultLocation(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.UseOsDefaultLocation(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetHeightTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetHeightTests.cs index db3a5dbef..2bffc7b9e 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetHeightTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetHeightTests.cs @@ -14,7 +14,7 @@ public class SetHeightTests { [Arguments(620)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetHeight(value); @@ -32,7 +32,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(640)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetHeight(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxHeightTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxHeightTests.cs index 7e7a4da45..590b8e6cb 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxHeightTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxHeightTests.cs @@ -14,7 +14,7 @@ public class SetMaxHeightTests { [Arguments(1000)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetMaxHeight(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(1080)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMaxHeight(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxSizeTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxSizeTests.cs index 69595dba0..ff8916b1b 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxSizeTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxSizeTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetMaxSizeTests { [Arguments(1920, 1080)] public async Task AtBuilderStage_DirectAssignment(int width, int height, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetMaxSize(width, height); @@ -32,7 +32,7 @@ public async Task AtBuilderStage_DirectAssignment(int width, int height, Cancell [Arguments(2000, 1120)] public async Task AtBuilderStage_ExtensionAssignment(int width, int height, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMaxSize(width, height); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxWidthTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxWidthTests.cs index 3358b2500..bd29e2e91 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxWidthTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMaxWidthTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetMaxWidthTests { [Arguments(1800)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetMaxWidth(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(1900)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMaxWidth(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinHeightTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinHeightTests.cs index a0557ff1f..aee6ffd11 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinHeightTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinHeightTests.cs @@ -14,7 +14,7 @@ public class SetMinHeightTests { [Arguments(360)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetMinHeight(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(380)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMinHeight(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinSizeTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinSizeTests.cs index fb14b38c4..fbe56e7d4 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinSizeTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinSizeTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetMinSizeTests { [Arguments(500, 300)] public async Task AtBuilderStage_DirectAssignment(int width, int height, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetMinSize(width, height); @@ -32,7 +32,7 @@ public async Task AtBuilderStage_DirectAssignment(int width, int height, Cancell [Arguments(520, 320)] public async Task AtBuilderStage_ExtensionAssignment(int width, int height, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMinSize(width, height); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinWidthTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinWidthTests.cs index 798fa66bc..60c522bb9 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinWidthTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetMinWidthTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetMinWidthTests { [Arguments(520)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetMinWidth(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(540)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMinWidth(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetResizableTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetResizableTests.cs index 5a59dc609..2ded166bc 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetResizableTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetResizableTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetResizableTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetResizable(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetResizable(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetSizeTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetSizeTests.cs index 1897aabf9..8c6b2e92c 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetSizeTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetSizeTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetSizeTests { [Arguments(900, 540)] public async Task AtBuilderStage_DirectAssignment(int width, int height, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetSize(width, height); @@ -34,7 +34,7 @@ public async Task AtBuilderStage_DirectAssignment(int width, int height, Cancell [Arguments(1024, 768)] public async Task AtBuilderStage_ExtensionAssignment(int width, int height, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); System.Drawing.Size value = new(width, height); // Act diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetWidthTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetWidthTests.cs index 96f65dcb7..f2fa46eea 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetWidthTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/SetWidthTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class SetWidthTests { [Arguments(980)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.SetWidth(value); @@ -32,7 +32,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(1000)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetWidth(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Size/UseOsDefaultSizeTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Size/UseOsDefaultSizeTests.cs index 3195e2bc3..0d437abfb 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Size/UseOsDefaultSizeTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Size/UseOsDefaultSizeTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -14,7 +14,7 @@ public class UseOsDefaultSizeTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.Size.UseOsDefaultSize(value); @@ -30,7 +30,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.UseOsDefaultSize(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/State/FullScreenTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/State/FullScreenTests.cs index b10aadc01..2dca288f8 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/State/FullScreenTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/State/FullScreenTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -15,7 +15,7 @@ public class FullScreenTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.SetFullScreen(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetFullScreen(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/State/MaximizedTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/State/MaximizedTests.cs index 08d94ca39..842d886a2 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/State/MaximizedTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/State/MaximizedTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -15,7 +15,7 @@ public class MaximizedTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.SetMaximized(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMaximized(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/State/MinimizedTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/State/MinimizedTests.cs index b051f40e4..66a9c4855 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/State/MinimizedTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/State/MinimizedTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -15,7 +15,7 @@ public class MinimizedTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.SetMinimized(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetMinimized(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/State/TopMostTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/State/TopMostTests.cs index 730078319..4b047d4b2 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/State/TopMostTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/State/TopMostTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -15,7 +15,7 @@ public class TopMostTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.SetTopMost(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetTopMost(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorBoundaryTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorBoundaryTests.cs index 8fb030bc1..6819b9a2c 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorBoundaryTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorBoundaryTests.cs @@ -15,7 +15,7 @@ public class ZoomFactorBoundaryTests { [Arguments(500)] public async Task Builder_StoresValidZoomRange(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.SetZoomFactor(value); @@ -31,7 +31,7 @@ public async Task Builder_StoresValidZoomRange(int value, CancellationToken ct) [Arguments(999)] public async Task Builder_StoresOutOfRangeZoom(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.SetZoomFactor(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorTests.cs index be1e50b8b..9c6ed257e 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomFactorTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -17,7 +17,7 @@ public class ZoomFactorTests { [Arguments(200)] public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.SetZoomFactor(value); @@ -35,7 +35,7 @@ public async Task AtBuilderStage_DirectAssignment(int value, CancellationToken c [Arguments(200)] public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.SetZoomFactor(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomTests.cs index 69bfe062b..077620c36 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/State/ZoomTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -15,7 +15,7 @@ public class ZoomTests { [Arguments(false)] public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act builder.Features.State.EnableZoom(value); @@ -31,7 +31,7 @@ public async Task AtBuilderStage_DirectAssignment(bool value, CancellationToken [Arguments(false)] public async Task AtBuilderStage_ExtensionAssignment(bool value, CancellationToken ct) { // Arrange - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); // Act IInfiniFrameWindowBuilder returnedBuilder = builder.EnableZoom(value); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/GetMessageWebMessageHandlerTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/GetMessageWebMessageHandlerTests.cs index d95deec45..bec49f299 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/GetMessageWebMessageHandlerTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/GetMessageWebMessageHandlerTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using System.Text.Json; @@ -150,7 +150,7 @@ await Assert.That(payload.GetProperty("error").GetString()) } private static (InfiniFrameWindowBuilder Builder, InfiniFrameEvents Events, RecordingInfiniFrameWindowSubstitute Window) CreateWindowHarness() { - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/MessageHandlersTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/MessageHandlersTests.cs index cf65d4813..3c3d1a240 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/MessageHandlersTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/MessageHandlersTests.cs @@ -1,4 +1,4 @@ -ο»Ώ// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; @@ -81,7 +81,7 @@ public async Task TitleChanged_WithoutPayload_DoesNotInvokeWindowMutation(Cancel } private static (InfiniFrameWindowBuilder Builder, InfiniFrameEvents Events, RecordingInfiniFrameWindowSubstitute Window) CreateWindowHarness() { - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandlerTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandlerTests.cs index 4df082940..1693902cd 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandlerTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandlerTests.cs @@ -128,7 +128,7 @@ public async Task HandleWebMessage_PublicUrl_Http_OpensBrowser(CancellationToken // Arrange RecordingExternalProcessLauncher launcher = new(); IServiceProvider serviceProvider = CreateServiceProvider(launcher); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.RegisterOpenExternalTargetWebMessageHandler(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; var events = new InfiniFrameEvents(eventsStore, NullLogger.Instance); @@ -154,7 +154,7 @@ public async Task HandleWebMessage_PublicUrl_Https_OpensBrowser(CancellationToke // Arrange RecordingExternalProcessLauncher launcher = new(); IServiceProvider serviceProvider = CreateServiceProvider(launcher); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.RegisterOpenExternalTargetWebMessageHandler(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; @@ -177,7 +177,7 @@ public async Task HandleWebMessage_MailtoUri_OpensMailClient(CancellationToken c // Arrange RecordingExternalProcessLauncher launcher = new(); IServiceProvider serviceProvider = CreateServiceProvider(launcher); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.RegisterOpenExternalTargetWebMessageHandler(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; @@ -222,7 +222,7 @@ public async Task HandleWebMessage_PrivateIp_Boundary172_15x_IsAllowed(Cancellat // Arrange: 172.15.x.x is NOT in the 172.16.0.0/12 private range RecordingExternalProcessLauncher launcher = new(); IServiceProvider serviceProvider = CreateServiceProvider(launcher); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.RegisterOpenExternalTargetWebMessageHandler(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; @@ -243,7 +243,7 @@ public async Task HandleWebMessage_PrivateIp_Boundary172_32x_IsAllowed(Cancellat // Arrange: 172.32.x.x is NOT in the 172.16.0.0/12 private range RecordingExternalProcessLauncher launcher = new(); IServiceProvider serviceProvider = CreateServiceProvider(launcher); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.RegisterOpenExternalTargetWebMessageHandler(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; @@ -264,7 +264,7 @@ public async Task HandleWebMessage_NonPrivate_9x_Ip_IsAllowed(CancellationToken // Arrange: 9.x.x.x is not in any private range RecordingExternalProcessLauncher launcher = new(); IServiceProvider serviceProvider = CreateServiceProvider(launcher); - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); builder.RegisterOpenExternalTargetWebMessageHandler(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; @@ -291,7 +291,7 @@ private static IServiceProvider CreateServiceProvider(IExternalProcessLauncher l } private static (InfiniFrameWindowBuilder Builder, InfiniFrameEvents Events, RecordingInfiniFrameWindowSubstitute Window) CreateWindowHarness() { - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() @@ -346,6 +346,7 @@ private sealed class WindowWithServiceProviderStub(IServiceProvider serviceProvi void IInfiniFrameWindow.MarkNativeHandleReleased() => throw new NotSupportedException(); void IInfiniFrameWindow.MarkDisposed() => throw new NotSupportedException(); void IInfiniFrameWindow.ReleaseNativeHandle() => throw new NotSupportedException(); + void IInfiniFrameWindow.MarkNativeHandleSafeToDestroy() => throw new NotSupportedException(); void IInfiniFrameWindow.SetManagedThreadId(int managedThreadId) => throw new NotSupportedException(); } } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/TitleChangedWebMessageHandlerTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/TitleChangedWebMessageHandlerTests.cs index 2082a994f..66181b1b0 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/TitleChangedWebMessageHandlerTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/TitleChangedWebMessageHandlerTests.cs @@ -68,7 +68,7 @@ public async Task HandleWebMessage_DifferentPayload_UpdatesTitle(CancellationTok // Helper Methods // ----------------------------------------------------------------------------------------------------------------- private static (InfiniFrameWindowBuilder Builder, InfiniFrameEvents Events, RecordingInfiniFrameWindowSubstitute Window) CreateWindowHarness() { - var builder = InfiniFrameWindowBuilder.Create(); + var builder = new InfiniFrameWindowBuilder(); var eventsStore = (InfiniFrameEventsStore)builder.EventsStore; RecordingInfiniFrameWindowSubstitute window = new RecordingInfiniFrameWindowSubstitute() diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs index 0b72e1189..33c9c5034 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs @@ -108,19 +108,36 @@ public async Task EveryPostCommand_InvokesTheManifestMethod() { } private static bool WasMethodCalled(object mockObj, string methodName) { - if (mockObj is Mock m1) return Mock.Invocations(m1).Any(c => c.MemberName == methodName); - if (mockObj is Mock m2) return Mock.Invocations(m2).Any(c => c.MemberName == methodName); - if (mockObj is Mock m3) return Mock.Invocations(m3).Any(c => c.MemberName == methodName); - if (mockObj is Mock m4) return Mock.Invocations(m4).Any(c => c.MemberName == methodName); - if (mockObj is Mock m5) return Mock.Invocations(m5).Any(c => c.MemberName == methodName); - if (mockObj is Mock m6) return Mock.Invocations(m6).Any(c => c.MemberName == methodName); - if (mockObj is Mock m7) return Mock.Invocations(m7).Any(c => c.MemberName == methodName); - if (mockObj is Mock m8) return Mock.Invocations(m8).Any(c => c.MemberName == methodName); - if (mockObj is Mock m9) return Mock.Invocations(m9).Any(c => c.MemberName == methodName); - if (mockObj is Mock m10) return Mock.Invocations(m10).Any(c => c.MemberName == methodName); - if (mockObj is Mock m11) return Mock.Invocations(m11).Any(c => c.MemberName == methodName); - if (mockObj is Mock m12) return Mock.Invocations(m12).Any(c => c.MemberName == methodName); - return false; + switch (mockObj) + { + case Mock m1: + return Mock.Invocations(m1).Any(c => c.MemberName == methodName); + case Mock m2: + return Mock.Invocations(m2).Any(c => c.MemberName == methodName); + case Mock m3: + return Mock.Invocations(m3).Any(c => c.MemberName == methodName); + case Mock m4: + return Mock.Invocations(m4).Any(c => c.MemberName == methodName); + case Mock m5: + return Mock.Invocations(m5).Any(c => c.MemberName == methodName); + case Mock m6: + return Mock.Invocations(m6).Any(c => c.MemberName == methodName); + case Mock m7: + return Mock.Invocations(m7).Any(c => c.MemberName == methodName); + case Mock m8: + return Mock.Invocations(m8).Any(c => c.MemberName == methodName); + case Mock m9: + return Mock.Invocations(m9).Any(c => c.MemberName == methodName); + case Mock m10: + return Mock.Invocations(m10).Any(c => c.MemberName == methodName); + case Mock m11: + return Mock.Invocations(m11).Any(c => c.MemberName == methodName); + case Mock m12: + return Mock.Invocations(m12).Any(c => c.MemberName == methodName); + default: + return false; + } + } private static (IInfiniFrameWindow Window, object Feature) CreateWindow(string featureName) { diff --git a/tests/InfiniTests/InfiniFrameTestServer.cs b/tests/InfiniTests/InfiniFrameTestServer.cs index b3a7e718d..f5ed0be03 100644 --- a/tests/InfiniTests/InfiniFrameTestServer.cs +++ b/tests/InfiniTests/InfiniFrameTestServer.cs @@ -72,7 +72,10 @@ public static InfiniFrameTestServer Create( TaskCreationOptions.RunContinuationsAsynchronously); var thread = new Thread(() => { try { - InfiniFrameWebApplicationBuilder builder = InfiniFrameWebApplication.CreateBuilder(); + InfiniFrameWebApplicationBuilder builder = new() { + WebApp = WebApplication.CreateBuilder(), + WindowBuilder = new InfiniFrameWindowBuilder() + }; builder.WebApp.WebHost.UseStaticWebAssets(); appBuilder?.Invoke(builder.WebApp); diff --git a/tests/InfiniTests/InfiniFrameTestWindow.cs b/tests/InfiniTests/InfiniFrameTestWindow.cs index a446c104e..005d4b969 100644 --- a/tests/InfiniTests/InfiniFrameTestWindow.cs +++ b/tests/InfiniTests/InfiniFrameTestWindow.cs @@ -99,7 +99,7 @@ public static InfiniFrameTestWindow Create(CancellationToken cancellationToken = public static InfiniFrameTestWindow Create(Action? builder = null, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var windowBuilder = InfiniFrameWindowBuilder.Create(); + var windowBuilder = new InfiniFrameWindowBuilder(); windowBuilder .SetIconFile("wwwroot/favicon.ico") .SetStartPageContent(StartString); diff --git a/tests/InfiniTests/MockFactory.cs b/tests/InfiniTests/MockFactory.cs index 91974d5b4..d6cd44791 100644 --- a/tests/InfiniTests/MockFactory.cs +++ b/tests/InfiniTests/MockFactory.cs @@ -44,4 +44,5 @@ public static class MockFactory { public static Mock CreateServiceProviderMock() => Mock.Of(); public static Mock CreateDisposableMock() => Mock.Of(); public static Mock> CreateValidatorMock() => Mock.Of>(); + public static Mock CreateApplicationMock() => Mock.Of(); }