diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e7ad7ab..14de4d0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,7 +7,7 @@ AdaptiveRemote is a remote control application for TV and AV equipment designed - Support for both touch/mouse and specialized input devices ## Technology Stack -- **Platform:** Windows OS only (.NET 8 / net8.0-windows) +- **Platform:** Windows OS only (.NET 10 / net10.0-windows) - **UI Framework:** WPF with Blazor WebView components (Microsoft.AspNetCore.Components.WebView.Wpf) - **Language:** C# with nullable reference types enabled - **Build System:** .NET SDK, MSBuild @@ -60,7 +60,7 @@ The AdaptiveRemote.Headless host uses Playwright for cross-platform E2E testing ``` - **Install Playwright Browsers (one-time):** ```bash - pwsh src/AdaptiveRemote.Headless/bin/Debug/net8.0/playwright.ps1 install chromium + pwsh src/AdaptiveRemote.Headless/bin/Debug/net10.0/playwright.ps1 install chromium ``` - **Run Tests:** No special environment needed - fully headless ```bash diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d4a85eb..9bac75b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Restore dependencies run: dotnet restore @@ -35,7 +35,7 @@ jobs: - name: Install Chromium browser for Playwright headless tests shell: pwsh - run: pwsh ./src/AdaptiveRemote.Headless/bin/Debug/net8.0/playwright.ps1 install chromium + run: pwsh ./src/AdaptiveRemote.Headless/bin/Debug/net10.0/playwright.ps1 install chromium - name: Test run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults" diff --git a/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj b/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj index 66c1310..e70d5af 100644 --- a/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj +++ b/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable AdaptiveRemote diff --git a/src/AdaptiveRemote.App/Services/IFileSystemExtensions.cs b/src/AdaptiveRemote.App/Services/IFileSystemExtensions.cs index 71e1679..bb09615 100644 --- a/src/AdaptiveRemote.App/Services/IFileSystemExtensions.cs +++ b/src/AdaptiveRemote.App/Services/IFileSystemExtensions.cs @@ -23,11 +23,19 @@ public static Stream OpenWrite(this IFileSystem fileSystem, string path, bool cr { if (createDirectory) { - CreateDirectory(fileSystem, DirectoryFor(path), recursive: true); + string? directory = DirectoryFor(path); + if (!string.IsNullOrEmpty(directory)) + { + CreateDirectory(fileSystem, directory, recursive: true); + } } return fileSystem.OpenWrite(path); } - private static string DirectoryFor(string path) => Path.GetDirectoryName(path)!; + private static string? DirectoryFor(string path) + { + string? directory = Path.GetDirectoryName(path); + return string.IsNullOrEmpty(directory) ? null : directory; + } } diff --git a/src/AdaptiveRemote.Console/AdaptiveRemote.Console.csproj b/src/AdaptiveRemote.Console/AdaptiveRemote.Console.csproj index bd3d6ea..60fc9ad 100644 --- a/src/AdaptiveRemote.Console/AdaptiveRemote.Console.csproj +++ b/src/AdaptiveRemote.Console/AdaptiveRemote.Console.csproj @@ -2,7 +2,7 @@ Exe - net8.0-windows7.0 + net10.0-windows7.0 enable enable true diff --git a/src/AdaptiveRemote.Headless/AdaptiveRemote.Headless.csproj b/src/AdaptiveRemote.Headless/AdaptiveRemote.Headless.csproj index df704d5..695eb35 100644 --- a/src/AdaptiveRemote.Headless/AdaptiveRemote.Headless.csproj +++ b/src/AdaptiveRemote.Headless/AdaptiveRemote.Headless.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/src/AdaptiveRemote.Headless/Components/App.razor b/src/AdaptiveRemote.Headless/Components/App.razor new file mode 100644 index 0000000..3c651ed --- /dev/null +++ b/src/AdaptiveRemote.Headless/Components/App.razor @@ -0,0 +1,20 @@ +@using AdaptiveRemote.Components + + + + + + + + + + + + + + + + + + + diff --git a/src/AdaptiveRemote.Headless/Components/Routes.razor b/src/AdaptiveRemote.Headless/Components/Routes.razor new file mode 100644 index 0000000..3a5898c --- /dev/null +++ b/src/AdaptiveRemote.Headless/Components/Routes.razor @@ -0,0 +1,3 @@ +@using AdaptiveRemote.Components + + diff --git a/src/AdaptiveRemote.Headless/Components/_Imports.razor b/src/AdaptiveRemote.Headless/Components/_Imports.razor new file mode 100644 index 0000000..1ee0523 --- /dev/null +++ b/src/AdaptiveRemote.Headless/Components/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using AdaptiveRemote.Headless +@using AdaptiveRemote.Headless.Components diff --git a/src/AdaptiveRemote.Headless/Pages/_Host.cshtml b/src/AdaptiveRemote.Headless/Pages/_Host.cshtml index d5f2396..3feb9cc 100644 --- a/src/AdaptiveRemote.Headless/Pages/_Host.cshtml +++ b/src/AdaptiveRemote.Headless/Pages/_Host.cshtml @@ -1,5 +1,6 @@ @* This page needs to be kept in sync with AdaptiveRemote.App\wwwroot\index.html *@ @page +@using Microsoft.AspNetCore.Components.Web @using AdaptiveRemote.Components @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @@ -11,17 +12,11 @@ + + -
- -
- -
- An unhandled error has occurred. - Reload - 🗙 -
+ \ No newline at end of file diff --git a/src/AdaptiveRemote.Headless/PlaywrightBrowserLifetimeService.cs b/src/AdaptiveRemote.Headless/PlaywrightBrowserLifetimeService.cs index 2eeee46..1d64f21 100644 --- a/src/AdaptiveRemote.Headless/PlaywrightBrowserLifetimeService.cs +++ b/src/AdaptiveRemote.Headless/PlaywrightBrowserLifetimeService.cs @@ -1,5 +1,7 @@ using AdaptiveRemote.Services.Testing; using AdaptiveRemote.Utilities; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.Options; using Microsoft.Playwright; @@ -13,6 +15,7 @@ internal class PlaywrightBrowserLifetimeService : BackgroundService, IBrowserUIA { private readonly ILogger _logger; private readonly IHostApplicationLifetime _lifetime; + private readonly IServer _server; private readonly PlaywrightSettings _settings; private IPlaywright? _playwright; private IBrowser? _browser; @@ -27,10 +30,12 @@ internal class PlaywrightBrowserLifetimeService : BackgroundService, IBrowserUIA public PlaywrightBrowserLifetimeService( ILogger logger, IOptions options, - IHostApplicationLifetime lifetime) + IHostApplicationLifetime lifetime, + IServer server) { _logger = logger; _lifetime = lifetime; + _server = server; _settings = options.Value; } @@ -43,8 +48,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) try { - // Get the port the app is listening on (from configuration or default) - string appUrl = "http://localhost:5000"; // Default + // Get the actual URL the server is listening on + var serverAddressesFeature = _server.Features.Get(); + string appUrl = serverAddressesFeature?.Addresses.FirstOrDefault() ?? "http://localhost:5000"; _logger.LogInformation("Will navigate to: {AppUrl}", appUrl); diff --git a/src/AdaptiveRemote.Headless/Program.cs b/src/AdaptiveRemote.Headless/Program.cs index 0bc8cce..74d7f6f 100644 --- a/src/AdaptiveRemote.Headless/Program.cs +++ b/src/AdaptiveRemote.Headless/Program.cs @@ -1,4 +1,5 @@ using AdaptiveRemote.Headless; +using AdaptiveRemote.Headless.Components; using AdaptiveRemote.Services.Conversation; using AdaptiveRemote.Services.Lifecycle; using AdaptiveRemote.Services.Testing; @@ -22,9 +23,9 @@ .AddSingleton() .AddSingleton(); -// Add the minimal services for ASP.NET to serve the Blazor UI -builder.Services.AddRazorPages(); -builder.Services.AddServerSideBlazor(); +// Add services to the container - using the .NET 10 template approach +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); // Register circuit handler for logging builder.Services.AddSingleton(); @@ -38,15 +39,16 @@ WebApplication app = builder.Build(); +// Configure the HTTP request pipeline - following .NET 10 template pattern if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Error"); + app.UseExceptionHandler("/Error", createScopeForErrors: true); } -app.UseStaticFiles(); -app.UseRouting(); +app.UseAntiforgery(); -app.MapBlazorHub(); -app.MapFallbackToPage("/_Host"); +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); app.Run(); diff --git a/src/AdaptiveRemote/AdaptiveRemote.csproj b/src/AdaptiveRemote/AdaptiveRemote.csproj index 92f9d27..6041b14 100644 --- a/src/AdaptiveRemote/AdaptiveRemote.csproj +++ b/src/AdaptiveRemote/AdaptiveRemote.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows + net10.0-windows win-x64 enable enable diff --git a/test/AdaptiveRemote.App.Tests/AdaptiveRemote.App.Tests.csproj b/test/AdaptiveRemote.App.Tests/AdaptiveRemote.App.Tests.csproj index e97ea59..17a6451 100644 --- a/test/AdaptiveRemote.App.Tests/AdaptiveRemote.App.Tests.csproj +++ b/test/AdaptiveRemote.App.Tests/AdaptiveRemote.App.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/test/AdaptiveRemote.App.Tests/Services/FileSystemExtensionsTests.cs b/test/AdaptiveRemote.App.Tests/Services/FileSystemExtensionsTests.cs index e9fdc8a..43660c4 100644 --- a/test/AdaptiveRemote.App.Tests/Services/FileSystemExtensionsTests.cs +++ b/test/AdaptiveRemote.App.Tests/Services/FileSystemExtensionsTests.cs @@ -5,6 +5,11 @@ public class FileSystemExtensionsTests { private readonly MockFileSystem MockFileSystem = new(); + // Platform-agnostic path helpers + private static string GetTestRoot() => Path.Combine("users", "bob_the_builder"); + private static string GetTestPath() => Path.Combine("users", "bob_the_builder", "temp"); + private static string GetTestFilePath() => Path.Combine("users", "bob_the_builder", "temp", "hat.txt"); + [TestCleanup] public void VerifyMocks() { @@ -15,7 +20,7 @@ public void VerifyMocks() public void FileSystemExtensions_CreateDirectory_Recursive_DoesNothingIfDirectoryExists() { // Arrange - const string input = @"C:\users\bob_the_builder\temp"; + string input = GetTestPath(); MockFileSystem.AddDirectory(input); MockFileSystem.Expect_CreateDirectory_IsNotCalled(); @@ -32,8 +37,8 @@ public void FileSystemExtensions_CreateDirectory_Recursive_DoesNothingIfDirector public void FileSystemExtensions_CreateDirectory_Recursive_CreatesOneLevelOfDirectory() { // Arrange - const string parent = @"C:\users\bob_the_builder"; - const string input = @"C:\users\bob_the_builder\temp"; + string parent = GetTestRoot(); + string input = GetTestPath(); MockFileSystem.AddDirectory(parent); IFileSystem fileSystem = MockFileSystem.Object; @@ -49,10 +54,10 @@ public void FileSystemExtensions_CreateDirectory_Recursive_CreatesOneLevelOfDire public void FileSystemExtensions_CreateDirectory_Recursive_CreatesMultipleLevelsOfDirectory() { // Arrange - const string parent3 = @"C:\"; - const string parent2 = @"C:\users"; - const string parent1 = @"C:\users\bob_the_builder"; - const string input = @"C:\users\bob_the_builder\temp"; + string parent3 = "users"; + string parent2 = GetTestRoot(); + string parent1 = GetTestPath(); + string input = Path.Combine("users", "bob_the_builder", "temp", "deep"); MockFileSystem.AddDirectory(parent3); IFileSystem fileSystem = MockFileSystem.Object; @@ -67,28 +72,19 @@ public void FileSystemExtensions_CreateDirectory_Recursive_CreatesMultipleLevels } [TestMethod] - public void FileSystemExtensions_CreateDirectory_Recursive_ThrowsArgumentExceptionForInvalidPath() + public void FileSystemExtensions_CreateDirectory_Recursive_CreatesRootLevelDirectory() { - // Arrange - const string input = @"C:\users\bob_the_builder\temp"; - - MockFileSystem.Expect_CreateDirectory_IsNotCalled(); - MockFileSystem.Expect_CreateDirectory_ForPath(@"C:\"); - + // Arrange - This test verifies that root-level directories can be created + // (which replaces the old test that expected this to fail on Windows) + string rootDir = "users"; + IFileSystem fileSystem = MockFileSystem.Object; - try - { - // Act - fileSystem.CreateDirectory(input, recursive: true); + // Act + fileSystem.CreateDirectory(rootDir, recursive: true); - // Assert - Assert.Fail("Expected exception was not thrown."); - } - catch (AssertFailedException result) when (result.Message == @"Assert.IsNotNull failed. Could not compute the parent path for 'C:\'") - { - // This is the expected exception - } + // Assert + Assert.IsTrue(fileSystem.DirectoryExists(rootDir), "Root directory {0} should have been created", rootDir); } [TestMethod] @@ -96,7 +92,7 @@ public void FileSystemExtensions_CreateDirectory_Recursive_ThrowsArgumentExcepti public void FileSystemExtensions_CreateDirectory_NotRecursive_ThrowsArgumentExceptionForInvalidPath() { // Arrange - const string input = @"C:\users\bob_the_builder\temp"; + string input = GetTestPath(); MockFileSystem.Expect_CreateDirectory_IsNotCalled(); MockFileSystem.Expect_CreateDirectory_ForPath(input); @@ -111,9 +107,9 @@ public void FileSystemExtensions_CreateDirectory_NotRecursive_ThrowsArgumentExce // Assert Assert.Fail("Expected exception was not thrown."); } - catch (AssertFailedException result) when (result.Message == @"Assert.IsTrue failed. Parent path 'C:\users\bob_the_builder' does not exist when attempting to create 'C:\users\bob_the_builder\temp'") + catch (AssertFailedException result) when (result.Message.Contains("Parent path") && result.Message.Contains("does not exist when attempting to create")) { - // This is the expected exception + // This is the expected exception - parent directory doesn't exist } } @@ -121,9 +117,10 @@ public void FileSystemExtensions_CreateDirectory_NotRecursive_ThrowsArgumentExce public void FileSystemExtensions_OpenWrite_CreateDirectory_DoesNotCreateDirectoryThatAlreadyExists() { // Arrange - const string input = @"C:\users\bob_the_builder\temp\hat.txt"; + string input = GetTestFilePath(); + string directory = Path.GetDirectoryName(input)!; - MockFileSystem.AddDirectory(@"C:\users\bob_the_builder\temp"); + MockFileSystem.AddDirectory(directory); MockFileSystem.Expect_CreateDirectory_IsNotCalled(); MockFileSystem.Expect_OpenWrite_ForPath(input); @@ -140,9 +137,10 @@ public void FileSystemExtensions_OpenWrite_CreateDirectory_DoesNotCreateDirector public void FileSystemExtensions_OpenWrite_CreateDirectory_CreatesDirectoryThatDoesNotExist() { // Arrange - const string input = @"C:\users\bob_the_builder\temp\hat.txt"; + string input = GetTestFilePath(); + string rootDir = "users"; - MockFileSystem.AddDirectory(@"C:\users"); + MockFileSystem.AddDirectory(rootDir); MockFileSystem.Expect_OpenWrite_ForPath(input); IFileSystem fileSystem = MockFileSystem.Object; @@ -152,17 +150,18 @@ public void FileSystemExtensions_OpenWrite_CreateDirectory_CreatesDirectoryThatD // Assert Assert.IsNotNull(resultStream, nameof(resultStream)); - Assert.IsTrue(fileSystem.DirectoryExists(@"C:\users\bob_the_builder"), "bob_the_builder does not exit"); - Assert.IsTrue(fileSystem.DirectoryExists(@"C:\users\bob_the_builder\temp"), "temp does not exist"); + Assert.IsTrue(fileSystem.DirectoryExists(GetTestRoot()), "bob_the_builder does not exist"); + Assert.IsTrue(fileSystem.DirectoryExists(GetTestPath()), "temp does not exist"); } [TestMethod] public void FileSystemExtensions_OpenWrite_DoNotCreateDirectory_ThrowsForDirectoryNotFound() { // Arrange - const string input = @"C:\users\bob_the_builder\temp\hat.txt"; + string input = GetTestFilePath(); + string rootDir = "users"; - MockFileSystem.AddDirectory(@"C:\users"); + MockFileSystem.AddDirectory(rootDir); MockFileSystem.Expect_OpenWrite_ForPath(input); MockFileSystem.Expect_CreateDirectory_IsNotCalled(); @@ -176,7 +175,7 @@ public void FileSystemExtensions_OpenWrite_DoNotCreateDirectory_ThrowsForDirecto // Assert Assert.Fail("Expected exception was not thrown"); } - catch (AssertFailedException result) when (result.Message == "Assert.IsTrue failed. Attempted to open a file for writing in directory that does not exist: C:\\users\\bob_the_builder\\temp\\hat.txt") + catch (AssertFailedException result) when (result.Message.Contains("Attempted to open a file for writing in directory that does not exist")) { // This is the expected exception } diff --git a/test/AdaptiveRemote.App.Tests/Services/Lifecycle/ApplicationLifecycleTests.cs b/test/AdaptiveRemote.App.Tests/Services/Lifecycle/ApplicationLifecycleTests.cs index d309fd8..2c0d98a 100644 --- a/test/AdaptiveRemote.App.Tests/Services/Lifecycle/ApplicationLifecycleTests.cs +++ b/test/AdaptiveRemote.App.Tests/Services/Lifecycle/ApplicationLifecycleTests.cs @@ -217,9 +217,16 @@ public void ApplicationLifecycle_StartAsync_DelayedFailure_LogsErrorAndDoesNotSt Task startTask = sut.StartAsync(default); + // In .NET 10, StartAsync returns immediately, so we need to wait for services to start initializing + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync should complete quickly"); + MockLogger.WaitForMessageAsync(Expect_InitializingMessage(MockService3), TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "Services should start initializing"); + // Act tcs.SetException(expectedError1); + // Wait for cleanup to complete + MockLogger.WaitForMessageAsync(Expect_CleanedUpMessage(MockService3), TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "Cleanup should complete after error"); + // Assert MockLogger.VerifyMessages( Expect_InitializingMessage(MockService1), @@ -235,7 +242,6 @@ public void ApplicationLifecycle_StartAsync_DelayedFailure_LogsErrorAndDoesNotSt Expect_CleaningUpMessage(MockService3), Expect_CleanedUpMessage(MockService3)); - startTask.Should().BeComplete(because: "StartAsync should complete after all services are initialized"); sut.ExecuteTask.Should().NotBeComplete(because: "ExecuteTask should remain running after startup"); LatestLifecyclePhase.Should().Be(LifecyclePhase.CleaningUp, because: "Services are being cleaned up after failure"); } @@ -258,7 +264,9 @@ public void ApplicationLifecycle_StartAsync_ErrorDuringConstructor_SetsFatalErro Task startTask = sut.StartAsync(default); // Assert - startTask.Should().BeComplete(because: "ApplicationLifecycle handles the exception and reports it as an error"); + // In .NET 10, StartAsync returns immediately + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync completes quickly"); + MockLogger.WaitForMessageAsync(Expect_ScopeConstructionFailed, TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "Error should be logged"); MockLogger.VerifyMessages( Expect_ScopeConstructionFailed); } @@ -277,14 +285,17 @@ public void ApplicationLifecycle_StopAsync_AfterErrorDuringConstructor_DoesNothi Expect_SetFatalErrorOn(MockLifecycleViewController, expectedError1); - sut.StartAsync(default) - .Should().BeComplete(because: "ApplicationLifecycle handles the exception and reports it as an error"); + Task startTask = sut.StartAsync(default); + + // In .NET 10, StartAsync returns immediately, wait for error to be logged + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync completes quickly"); + MockLogger.WaitForMessageAsync(Expect_ScopeConstructionFailed, TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "Error should be logged"); // Act Task stopTask = sut.StopAsync(default); // Assert - stopTask.Should().BeComplete(because: "StopAsync should have nothing to do"); + stopTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StopAsync should have nothing to do"); MockLogger.VerifyMessages( Expect_ScopeConstructionFailed, Expect_ShuttingDownMessage); @@ -339,6 +350,10 @@ public void ApplicationLifecycle_StopAsync_DisposesScopeAndCompletesTask() Task startTask = sut.StartAsync(default); + // In .NET 10, StartAsync returns immediately, so we need to wait for initialization to complete + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync should complete quickly"); + MockLogger.WaitForMessageAsync(Expect_InitializedMessage(MockService3), TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "All services should initialize"); + // Act Task stopTask = sut.StopAsync(default); @@ -358,9 +373,8 @@ public void ApplicationLifecycle_StopAsync_DisposesScopeAndCompletesTask() Expect_CleaningUpMessage(MockService3), Expect_CleanedUpMessage(MockService3)); - startTask.Should().BeComplete(because: "StartAsync should complete after all services are initialized"); - stopTask.Should().BeComplete(because: "StopAsync should complete after all services are cleaned up"); - sut.ExecuteTask.Should().BeComplete(because: "ExecuteTask should complete after all services have stopped"); + stopTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StopAsync should complete after all services are cleaned up"); + sut.ExecuteTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "ExecuteTask should complete after all services have stopped"); LatestLifecyclePhase.Should().Be(LifecyclePhase.CleaningUp, because: "we stay in this state after services are stopped, until the application exits"); } @@ -379,6 +393,10 @@ public void ApplicationLifecycle_StopAsync_BlocksUntilServicesAreCleanedUp() Task startTask = sut.StartAsync(default); + // In .NET 10, StartAsync returns immediately, so we need to wait for initialization to complete + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync should complete quickly"); + MockLogger.WaitForMessageAsync(Expect_InitializedMessage(MockService3), TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "All services should initialize"); + // Act Task stopTask = sut.StopAsync(default); @@ -395,7 +413,6 @@ public void ApplicationLifecycle_StopAsync_BlocksUntilServicesAreCleanedUp() Expect_CleaningUpMessage(MockService2), Expect_CleaningUpMessage(MockService3)); - startTask.Should().BeComplete(because: "StartAsync should complete after all services are initialized"); stopTask.Should().NotBeComplete(because: "StopAsync should block until all services are cleaned up"); sut.ExecuteTask.Should().NotBeComplete(because: "ExecuteTask should remain running after startup"); LatestLifecyclePhase.Should().Be(LifecyclePhase.CleaningUp, because: "Services are being cleaned up for StopAsync"); @@ -420,6 +437,10 @@ public void ApplicationLifecycle_StopAsync_ReportsErrorsInCleanUp() Task startTask = sut.StartAsync(default); + // In .NET 10, StartAsync returns immediately, so we need to wait for initialization to complete + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync should complete quickly"); + MockLogger.WaitForMessageAsync(Expect_InitializedMessage(MockService3), TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "All services should initialize"); + // Act Task stopTask = sut.StopAsync(default); @@ -439,9 +460,8 @@ public void ApplicationLifecycle_StopAsync_ReportsErrorsInCleanUp() Expect_CleaningUpMessage(MockService3), Expect_CleanedUpMessage(MockService3)); - startTask.Should().BeComplete(because: "StartAsync should complete after all services are initialized"); - stopTask.Should().BeComplete(because: "StopAsyc should complete after all services are cleaned up"); - sut.ExecuteTask.Should().BeComplete(because: "ExecuteTask should complete after all services have stopped"); + stopTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StopAsyc should complete after all services are cleaned up"); + sut.ExecuteTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "ExecuteTask should complete after all services have stopped"); LatestLifecyclePhase.Should().Be(LifecyclePhase.CleaningUp, because: "services are being cleaned up for StopAsync, even though there was an error"); } @@ -460,6 +480,10 @@ public void ApplicationLifecycle_StopAsync_CancelsInitializeMethodsThatAreWaitin Task startTask = sut.StartAsync(default); + // In .NET 10, StartAsync returns immediately, so we need to wait for initialization to start + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync should complete quickly"); + MockLogger.WaitForMessageAsync(Expect_InitializingMessage(MockService2), TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "MockService2 should start initializing"); + // Act Task stopTask = sut.StopAsync(default); @@ -478,9 +502,8 @@ public void ApplicationLifecycle_StopAsync_CancelsInitializeMethodsThatAreWaitin Expect_CleaningUpMessage(MockService3), Expect_CleanedUpMessage(MockService3)); - startTask.Should().BeComplete(because: "StartAsync should complete after all services are initialized"); - stopTask.Should().BeComplete(because: "StopAsync should complete after all services are cleaned up"); - sut.ExecuteTask.Should().BeComplete(because: "ExecuteTask should complete after all services have stoped"); + stopTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StopAsync should complete after all services are cleaned up"); + sut.ExecuteTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "ExecuteTask should complete after all services have stoped"); LatestLifecyclePhase.Should().Be(LifecyclePhase.CleaningUp, because: "we stay in this state after StopAsync until the application exits, even after services have cleaned up"); } @@ -500,8 +523,11 @@ public void ApplicationLifecycle_StopAsync_AfterInitializeFailure_DoesNothing() Expect_CleanupAsyncOn(MockService1); Expect_CleanupAsyncOn(MockService2); - sut.StartAsync(default) - .Should().BeComplete(because: "StartAsync should complete after all services are initialized"); + Task startTask = sut.StartAsync(default); + + // In .NET 10, StartAsync returns immediately, so we need to wait for initialization to complete + startTask.Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "StartAsync should complete quickly"); + MockLogger.WaitForMessageAsync(Expect_CleanedUpMessage(MockService2), TimeSpan.FromSeconds(1)).Should().BeCompleteWithin(TimeSpan.FromSeconds(1), because: "Cleanup should complete after initialization failure"); // Act Task stopTask = sut.StopAsync(default); diff --git a/test/AdaptiveRemote.App.Tests/Services/ProgrammaticSettings/PersistSettingsTests.cs b/test/AdaptiveRemote.App.Tests/Services/ProgrammaticSettings/PersistSettingsTests.cs index 85689bf..3a996c1 100644 --- a/test/AdaptiveRemote.App.Tests/Services/ProgrammaticSettings/PersistSettingsTests.cs +++ b/test/AdaptiveRemote.App.Tests/Services/ProgrammaticSettings/PersistSettingsTests.cs @@ -6,7 +6,7 @@ namespace AdaptiveRemote.Services.ProgrammaticSettings; [TestClass] public class PersistSettingsTests { - private const string InputSettingsPath = @"C:\path\to\settings.ini"; + private static readonly string InputSettingsPath = Path.Combine("path", "to", "settings.ini"); private readonly MockLogger MockLogger = new(); private readonly MockFileSystem MockFileSystem = new(); @@ -48,7 +48,7 @@ public async Task PersistSettings_Set_SavesSettingsToFileAsync() await MockLogger.WaitForMessageAsync(ExpectMessage_SavedSettings()); // Assert - MockFileSystem.VerifyFileContents(InputSettingsPath, "ExistingSetting=123\r\nNewSetting=abc\r\n"); + MockFileSystem.VerifyFileContents(InputSettingsPath, $"ExistingSetting=123{Environment.NewLine}NewSetting=abc{Environment.NewLine}"); MockLogger.VerifyMessages( ExpectMessage_LoadingExistingSettings(), @@ -113,7 +113,7 @@ public async Task PersistSettings_Set_ChangesExistingSettingInFileAsync() await MockLogger.WaitForMessageAsync(ExpectMessage_SavedSettings()); // Assert - MockFileSystem.VerifyFileContents(InputSettingsPath, "ExistingSetting=ghi\r\n"); + MockFileSystem.VerifyFileContents(InputSettingsPath, $"ExistingSetting=ghi{Environment.NewLine}"); MockLogger.VerifyMessages( ExpectMessage_LoadingExistingSettings(), @@ -195,7 +195,7 @@ public async Task PersistSettings_Set_WhenFileNotFound_CreatesFileAsync() await MockLogger.WaitForMessageAsync(ExpectMessage_SavedSettings()); // Assert - MockFileSystem.VerifyFileContents(InputSettingsPath, "NewSetting=abc\r\n"); + MockFileSystem.VerifyFileContents(InputSettingsPath, $"NewSetting=abc{Environment.NewLine}"); MockLogger.VerifyMessages( ExpectMessage_AddSetting("NewSetting", "abc"), @@ -209,12 +209,12 @@ public async Task PersistSettings_Set_WhenFileNotFound_CreatesDirectoryAsync() // Arrange IPersistSettings sut = CreateSut(); - MockFileSystem.AddDirectory(Path.GetPathRoot(InputSettingsPath)); + string rootPath = Path.GetDirectoryName(Path.GetDirectoryName(InputSettingsPath)!) ?? "path"; + MockFileSystem.AddDirectory(rootPath); MockFileSystem.Expect_OpenRead_IsNotCalled(); - MockFileSystem.Expect_CreateDirectory_ForPath(@"C:\path"); - MockFileSystem.Expect_CreateDirectory_ForPath(@"C:\path\to"); + MockFileSystem.Expect_CreateDirectory_ForPath(Path.Combine("path", "to")); MockFileSystem.Expect_OpenWrite_ForPath(InputSettingsPath); @@ -224,7 +224,7 @@ public async Task PersistSettings_Set_WhenFileNotFound_CreatesDirectoryAsync() await MockLogger.WaitForMessageAsync(ExpectMessage_SavedSettings()); // Assert - MockFileSystem.VerifyFileContents(InputSettingsPath, "NewSetting=abc\r\n"); + MockFileSystem.VerifyFileContents(InputSettingsPath, $"NewSetting=abc{Environment.NewLine}"); MockLogger.VerifyMessages( ExpectMessage_AddSetting("NewSetting", "abc"), diff --git a/test/AdaptiveRemote.App.Tests/TestUtilities/MockFileSystem.cs b/test/AdaptiveRemote.App.Tests/TestUtilities/MockFileSystem.cs index cb82bab..4ebe2df 100644 --- a/test/AdaptiveRemote.App.Tests/TestUtilities/MockFileSystem.cs +++ b/test/AdaptiveRemote.App.Tests/TestUtilities/MockFileSystem.cs @@ -28,15 +28,23 @@ public void AddFile(string path) => AddFile(path, $"Test File Content for {path}"); public void AddFile(string path, string content) { - AddDirectory(Path.GetDirectoryName(path)); + string? directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory)) + { + AddDirectory(directory); + } _files.Add(path, new MemoryStream(Encoding.UTF8.GetBytes(content))); } public void AddDirectory(string? path) { - if (path is not null && _directories.Add(path)) + if (!string.IsNullOrEmpty(path) && _directories.Add(path)) { - AddDirectory(Path.GetDirectoryName(path)); + string? parentDirectory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(parentDirectory)) + { + AddDirectory(parentDirectory); + } } } @@ -52,8 +60,10 @@ private void Callback_CreateDirectory(string path) Assert.IsFalse(_directories.Contains(path), "Attempted to create a directory that already exists: {0}", path); string? parent = Path.GetDirectoryName(path); - Assert.IsNotNull(parent, "Could not compute the parent path for '{0}'", path); - Assert.IsTrue(_directories.Contains(parent), "Parent path '{0}' does not exist when attempting to create '{1}'", parent, path); + if (!string.IsNullOrEmpty(parent)) + { + Assert.IsTrue(_directories.Contains(parent), "Parent path '{0}' does not exist when attempting to create '{1}'", parent, path); + } _directories.Add(path); } @@ -104,8 +114,10 @@ public void Expect_OpenWrite_IsNotCalled() private Stream Returns_OpenWrite(string path) { string? parentDirectory = Path.GetDirectoryName(path); - Assert.IsNotNull(parentDirectory, "Could not compute the parent path for '{0}'", path); - Assert.IsTrue(_directories.Contains(parentDirectory), "Attempted to open a file for writing in directory that does not exist: {0}", path); + if (!string.IsNullOrEmpty(parentDirectory)) + { + Assert.IsTrue(_directories.Contains(parentDirectory), "Attempted to open a file for writing in directory that does not exist: {0}", path); + } if (_files.TryGetValue(path, out Stream? existingStream) && existingStream is DoNotDisposeStream writeStream) diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/AdaptiveRemote.EndtoEndTests.TestServices.csproj b/test/AdaptiveRemote.EndtoEndTests.TestServices/AdaptiveRemote.EndtoEndTests.TestServices.csproj index 328a0f3..6dfbc8f 100644 --- a/test/AdaptiveRemote.EndtoEndTests.TestServices/AdaptiveRemote.EndtoEndTests.TestServices.csproj +++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/AdaptiveRemote.EndtoEndTests.TestServices.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable AdaptiveRemote.EndtoEndTests diff --git a/test/AdaptiveRemote.EndtoEndTests/AdaptiveRemote.EndtoEndTests.csproj b/test/AdaptiveRemote.EndtoEndTests/AdaptiveRemote.EndtoEndTests.csproj index 72ba7a3..cd7173d 100644 --- a/test/AdaptiveRemote.EndtoEndTests/AdaptiveRemote.EndtoEndTests.csproj +++ b/test/AdaptiveRemote.EndtoEndTests/AdaptiveRemote.EndtoEndTests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 latest enable enable diff --git a/test/AdaptiveRemote.EndtoEndTests/ConsoleHostTests.cs b/test/AdaptiveRemote.EndtoEndTests/ConsoleHostTests.cs index 226eda5..8ee7859 100644 --- a/test/AdaptiveRemote.EndtoEndTests/ConsoleHostTests.cs +++ b/test/AdaptiveRemote.EndtoEndTests/ConsoleHostTests.cs @@ -28,7 +28,7 @@ public static void ClassInitialize(TestContext context) protected override AdaptiveRemoteHostSettings GetHostSettings(string solutionRoot) => new( UIService: UIServiceType.BlazorWebView, - ExePath: Path.Combine(solutionRoot, "src/AdaptiveRemote.Console/bin/Debug/net8.0-windows7.0/AdaptiveRemote.Console.exe")); + ExePath: Path.Combine(solutionRoot, "src/AdaptiveRemote.Console/bin/Debug/net10.0-windows7.0/AdaptiveRemote.Console.exe")); protected override ILogger CreateTypedLogger(AdaptiveRemoteHost host) => host.CreateLogger(); diff --git a/test/AdaptiveRemote.EndtoEndTests/HeadlessHostTests.cs b/test/AdaptiveRemote.EndtoEndTests/HeadlessHostTests.cs index a9d97c2..a949772 100644 --- a/test/AdaptiveRemote.EndtoEndTests/HeadlessHostTests.cs +++ b/test/AdaptiveRemote.EndtoEndTests/HeadlessHostTests.cs @@ -29,7 +29,7 @@ protected override AdaptiveRemoteHostSettings GetHostSettings(string solutionRoo return new( UIService: UIServiceType.Playwright, - ExePath: Path.Combine(solutionRoot, $"src/AdaptiveRemote.Headless/bin/Debug/net8.0/{exeName}"), + ExePath: Path.Combine(solutionRoot, $"src/AdaptiveRemote.Headless/bin/Debug/net10.0/{exeName}"), CommandLineArgs: $"--playwright:TracesDir=\"{TracesPath}\""); } diff --git a/test/AdaptiveRemote.EndtoEndTests/WpfHostTests.cs b/test/AdaptiveRemote.EndtoEndTests/WpfHostTests.cs index 029f314..e601205 100644 --- a/test/AdaptiveRemote.EndtoEndTests/WpfHostTests.cs +++ b/test/AdaptiveRemote.EndtoEndTests/WpfHostTests.cs @@ -28,7 +28,7 @@ public static void ClassInitialize(TestContext context) protected override AdaptiveRemoteHostSettings GetHostSettings(string solutionRoot) => new( UIService: UIServiceType.BlazorWebView, - ExePath: Path.Combine(solutionRoot, "src/AdaptiveRemote/bin/Debug/net8.0-windows/AdaptiveRemote.exe")); + ExePath: Path.Combine(solutionRoot, "src/AdaptiveRemote/bin/Debug/net10.0-windows/AdaptiveRemote.exe")); protected override ILogger CreateTypedLogger(AdaptiveRemoteHost host) => host.CreateLogger(); diff --git a/test/AdaptiveRemote.Speech.Tests/AdaptiveRemote.Speech.Tests.csproj b/test/AdaptiveRemote.Speech.Tests/AdaptiveRemote.Speech.Tests.csproj index 11435da..1f34f84 100644 --- a/test/AdaptiveRemote.Speech.Tests/AdaptiveRemote.Speech.Tests.csproj +++ b/test/AdaptiveRemote.Speech.Tests/AdaptiveRemote.Speech.Tests.csproj @@ -1,7 +1,7 @@  - net8.0-windows + net10.0-windows latest enable enable