diff --git a/change/@ni-nimble-blazor-0f03cea0-79ac-44ba-983f-26623b2691f9.json b/change/@ni-nimble-blazor-0f03cea0-79ac-44ba-983f-26623b2691f9.json new file mode 100644 index 0000000000..8ee73aa7e1 --- /dev/null +++ b/change/@ni-nimble-blazor-0f03cea0-79ac-44ba-983f-26623b2691f9.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Acceptance/e2e tests for Nimble Blazor using Playwright (first pass)", + "packageName": "@ni/nimble-blazor", + "email": "20709258+msmithNI@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/package-lock.json b/package-lock.json index d3f75a1f1c..ffbd9501c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29981,6 +29981,7 @@ "packages/nimble-blazor": { "name": "@ni/nimble-blazor", "version": "11.8.30", + "hasInstallScript": true, "license": "MIT", "devDependencies": { "@microsoft/fast-web-utilities": "^6.0.0", @@ -29990,6 +29991,7 @@ "@rollup/plugin-node-resolve": "^15.0.1", "cross-env": "^7.0.3", "glob": "^8.1.0", + "playwright": "^1.30.0", "rollup": "^3.10.1" } }, @@ -34058,6 +34060,7 @@ "@rollup/plugin-node-resolve": "^15.0.1", "cross-env": "^7.0.3", "glob": "^8.1.0", + "playwright": "^1.30.0", "rollup": "^3.10.1" }, "dependencies": { diff --git a/packages/nimble-blazor/.gitignore b/packages/nimble-blazor/.gitignore index a61f755857..1932c22e06 100644 --- a/packages/nimble-blazor/.gitignore +++ b/packages/nimble-blazor/.gitignore @@ -2,6 +2,7 @@ NimbleBlazor/wwwroot/nimble-*/ NimbleBlazor/wwwroot/NimbleBlazor.HybridWorkaround.js NimbleBlazor/Components/Icons/ +build/generate-playwright-version-properties/dist/ artifacts/ bin/ obj/ diff --git a/packages/nimble-blazor/CONTRIBUTING.md b/packages/nimble-blazor/CONTRIBUTING.md index c9559ab4a8..a7aaa659bd 100644 --- a/packages/nimble-blazor/CONTRIBUTING.md +++ b/packages/nimble-blazor/CONTRIBUTING.md @@ -14,7 +14,7 @@ For Nimble Blazor development on Windows, the suggested tools to install are: - (Optional) Enable IIS (see "Enabling IIS", below) - ASP.NET Core Runtime 6.0.4xx: Choose "Hosting Bundle" under ASP.NET Core Runtime, on the [.NET 6.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -In Visual Studio, run either the `Demo.Server` or `Demo.Projects` to see the Blazor demo apps. +In Visual Studio, run either the `Demo.Server` or `Demo.Client` project to see the Blazor demo apps. ### Mac / Visual Studio Code Install [Visual Studio Code](https://code.visualstudio.com/), and install the suggested extensions that appear once you open the NimbleBlazor project folders. @@ -75,10 +75,26 @@ The C# code for a property supporting 2-way binding will look like this: ## Testing -### Automated +### Automated Unit Tests + +Test Project: `NimbleBlazor.Tests` Testing the Nimble Blazor components is possible through the use of xUnit and bUnit. Each Nimble Blazor component should have a corresponding test file. +### Automated Acceptance Tests + +Test Project: `NimbleBlazor.Tests.Acceptance` + +In order to fully test the Nimble Blazor components, consider writing new automated acceptance tests for new/modified components. Any component which requires custom JS code in `NimbleBlazor.lib.module.js` should generally have corresponding acceptance tests, because the bUnit tests in `NimbleBlazor.Tests` are unable to exercise/test that JavaScript code. + +The `NimbleBlazor.Tests.Acceptance` project starts a local Blazor.Server app which serves Razor pages that host the Nimble components. Then, xUnit-based acceptance tests start a Chromium instance using [Playwright](https://playwright.dev/), load those Razor pages, and interact with them. + +To add a new acceptance test: +- Add a new Razor file that uses that component in the `Pages` subfolder, with the name `[ComponentName][FunctionalityUnderTest].razor`, e.g. `DialogOpenAndClose.razor`. Add any necessary code to initialize the component in a `@code` section in the same file. If you'll interact with the component as the test runs, you may need to add other Nimble components like buttons to trigger new actions on your component under test. +- In the `Tests` subfolder, add a new class `[ComponentName]Tests.cs` if it doesn't already exist. Add a new test method in that class. Load your Razor file with the `NewPageForRouteAsync(routeName)` method. Using the Playwright APIs, interact with the components on the page, and make assertions about the state of the component under test. + +See the existing acceptance tests for examples of using the Playwright APIs. Additionally, see [Getting Started with Playwright Tests (Skyline End2EndTests)](https://dev.azure.com/ni/DevCentral/_git/Skyline?path=/End2EndTests/Getting%20Started%20with%20Playwright%20Tests.md&_a=preview) and the [Playwright .NET docs on writing tests](https://playwright.dev/dotnet/docs/writing-tests). + ### Example App / Manual Testing Each Nimble Blazor component should also be showcased in the `Demo` example projects. Simple component examples can be added directly in the `ComponentsDemo.razor` file (in the `Demo.Shared` project). The example project is very similar to the Nimble Angular example-client-app, and component demos can be adapted from that Angular app. Things to keep in mind that are specific to Blazor: diff --git a/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor b/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor index 27300948f8..1ae1e4a02a 100644 --- a/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor +++ b/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor @@ -5,7 +5,7 @@

Explore the components below to see the Nimble components in action. See the Nimble + href="https://nimble.ni.dev/storybook/?path=/docs/getting-started--docs">Nimble component docs for additional usage details.

diff --git a/packages/nimble-blazor/NimbleBlazor.sln b/packages/nimble-blazor/NimbleBlazor.sln index 93b9e1b50a..941b7bb07a 100644 --- a/packages/nimble-blazor/NimbleBlazor.sln +++ b/packages/nimble-blazor/NimbleBlazor.sln @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E5C31FAF EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Hybrid", "Examples\Demo.Hybrid\Demo.Hybrid.csproj", "{EAC50129-EF2E-4E7B-98D0-64502E97ED8B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NimbleBlazor.Tests.Acceptance", "Tests\NimbleBlazor.Tests.Acceptance\NimbleBlazor.Tests.Acceptance.csproj", "{7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,6 +51,10 @@ Global {EAC50129-EF2E-4E7B-98D0-64502E97ED8B}.Debug|Any CPU.Build.0 = Debug|Any CPU {EAC50129-EF2E-4E7B-98D0-64502E97ED8B}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAC50129-EF2E-4E7B-98D0-64502E97ED8B}.Release|Any CPU.Build.0 = Release|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,6 +65,7 @@ Global {6A1D0B77-BBF2-415E-B3A8-FAB00879F07C} = {638B1C16-782F-4C91-A09C-3569957356DF} {8B6E367C-E472-4E68-98D2-968CFCF6939D} = {638B1C16-782F-4C91-A09C-3569957356DF} {EAC50129-EF2E-4E7B-98D0-64502E97ED8B} = {638B1C16-782F-4C91-A09C-3569957356DF} + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B} = {E5C31FAF-7DEF-494F-A0D2-C9A4875F2132} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {38E2A588-0714-41E7-9BA3-D89622560FF9} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/App.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/App.razor new file mode 100644 index 0000000000..6fd3ed1b5a --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/BlazorServerWebHostFixture.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/BlazorServerWebHostFixture.cs new file mode 100644 index 0000000000..10206871f1 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/BlazorServerWebHostFixture.cs @@ -0,0 +1,18 @@ +namespace NimbleBlazor.Tests.Acceptance; + +/// +/// Test fixture which starts up a Blazor Server web server +/// +public class BlazorServerWebHostFixture : WebHostServerFixture +{ + protected override IHost CreateWebHost() + { + return new HostBuilder() + .ConfigureWebHost(webHostBuilder => webHostBuilder + .UseKestrel() + .UseStartup() + .UseStaticWebAssets() + .UseUrls("http://127.0.0.1:0")) // Pick a port dynamically + .Build(); + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/NimbleBlazor.Tests.Acceptance.csproj b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/NimbleBlazor.Tests.Acceptance.csproj new file mode 100644 index 0000000000..b2126d33bd --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/NimbleBlazor.Tests.Acceptance.csproj @@ -0,0 +1,42 @@ + + + + + net6.0 + enable + enable + false + + + + CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/DialogOpenAndClose.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/DialogOpenAndClose.razor new file mode 100644 index 0000000000..2fef2593d7 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/DialogOpenAndClose.razor @@ -0,0 +1,29 @@ +@page "/DialogOpenAndClose" +@namespace NimbleBlazor.Tests.Acceptance.Pages +@inherits LayoutComponentBase + +Open + + + Example Dialog + Close + + + + +@code { + private NimbleDialog? _dialog; + private NimbleTextField? _textField; + private string? DialogCloseReason { get; set; } + + public async Task OpenDialogAsync() + { + var response = await _dialog!.ShowAsync(); + DialogCloseReason = response.Value; + } + + public async Task CloseDialogAsync() + { + await _dialog!.CloseAsync("Custom Close Reason"); + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/DrawerOpenAndClose.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/DrawerOpenAndClose.razor new file mode 100644 index 0000000000..093d0a8083 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/DrawerOpenAndClose.razor @@ -0,0 +1,29 @@ +@page "/DrawerOpenAndClose" +@namespace NimbleBlazor.Tests.Acceptance.Pages +@inherits LayoutComponentBase + +Open + + + Example Drawer + Close + + + + +@code { + private NimbleDrawer? _drawer; + private NimbleTextField? _textField; + private string? DrawerCloseReason { get; set; } + + public async Task OpenDrawerAsync() + { + var response = await _drawer!.ShowAsync(); + DrawerCloseReason = response.Value; + } + + public async Task CloseDrawerAsync() + { + await _drawer!.CloseAsync("Custom Close Reason"); + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableBindToData.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableBindToData.razor new file mode 100644 index 0000000000..f214cc5276 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableBindToData.razor @@ -0,0 +1,43 @@ +@page "/TableBindToData" +@namespace NimbleBlazor.Tests.Acceptance.Pages +@using System.Diagnostics.CodeAnalysis; +@inherits LayoutComponentBase + + + Column 1 + + +@code { + [NotNull] + public IEnumerable TableData { get; set; } = Enumerable.Empty(); + + public TableBindToData() + { + UpdateTableData(5); + } + + public void UpdateTableData(int numberOfRows) + { + var tableData = new RowData[numberOfRows]; + for (int i = 0; i < numberOfRows; i++) + { + tableData[i] = new RowData( + i.ToString(null, null), + $"A{i}"); + } + + TableData = tableData; + } + + public class RowData + { + public RowData(string id, string field1) + { + Id = id; + Field1 = field1; + } + + public string Id { get; } + public string Field1 { get; } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/_Host.cshtml b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/_Host.cshtml new file mode 100644 index 0000000000..2f7c6bf6e7 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/_Host.cshtml @@ -0,0 +1,8 @@ +@page "/" +@namespace NimbleBlazor.Tests.Acceptance.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = "_Layout"; +} + + diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/_Layout.cshtml b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/_Layout.cshtml new file mode 100644 index 0000000000..ced5e4a5bd --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/_Layout.cshtml @@ -0,0 +1,33 @@ +@using Microsoft.AspNetCore.Components.Web +@namespace NimbleBlazor.Tests.Acceptance.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + + + @RenderBody() + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/PlaywrightFixture.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/PlaywrightFixture.cs new file mode 100644 index 0000000000..25ff2dccef --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/PlaywrightFixture.cs @@ -0,0 +1,45 @@ +using Microsoft.Playwright; +using Xunit; +using PlaywrightProgram = Microsoft.Playwright.Program; + +namespace NimbleBlazor.Tests.Acceptance; + +/// +/// Fixture to handle Playwright initialization for acceptance tests. +/// +public class PlaywrightFixture : IAsyncLifetime +{ + private IBrowser? _browser; + private IPlaywright? _playwright; + public IBrowserContext? BrowserContext { get; private set; } + + public async Task InitializeAsync() + { + _playwright = await Playwright.CreateAsync(); + _browser = await _playwright.Chromium.LaunchAsync( + new BrowserTypeLaunchOptions() + { +#if DEBUG + Headless = false, + SlowMo = 1000 +#endif + }); + BrowserContext = await _browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); +#if DEBUG + BrowserContext.SetDefaultTimeout(30000); +#endif + } + + public async Task DisposeAsync() + { + if (BrowserContext != null) + { + await BrowserContext.DisposeAsync(); + } + if (_browser != null) + { + await _browser.DisposeAsync(); + } + _playwright?.Dispose(); + } +} \ No newline at end of file diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Program.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Program.cs new file mode 100644 index 0000000000..a4ce042610 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Program.cs @@ -0,0 +1,21 @@ +namespace NimbleBlazor.Tests.Acceptance +{ + /// + /// Main entry point which spins up the web server and allows loading the Razor fixtures/pages in a browser + /// without running a specific test. + /// + public static class Program + { + public static void Main(string[] arguments) + { + var builder = WebApplication.CreateBuilder(arguments); + + var startup = new Startup(builder.Configuration); + startup.ConfigureServices(builder.Services); + var app = builder.Build(); + startup.Configure(app); + + app.Run(); + } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Properties/launchSettings.json b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Properties/launchSettings.json new file mode 100644 index 0000000000..129a55f7fa --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:44651" + } + }, + "profiles": { + "NimbleBlazor.Acceptance.Tests": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5202", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor new file mode 100644 index 0000000000..708f84232b --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor @@ -0,0 +1,20 @@ +@using Microsoft.AspNetCore.Components +@using NimbleBlazor +@namespace NimbleBlazor.Tests.Acceptance.Shared +@inherits LayoutComponentBase + +Nimble Blazor tests + +
+ + +
+ @Body +
+
+ +

@ex.Message

+
+
+
+
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor.cs new file mode 100644 index 0000000000..b51bec874f --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; +using NimbleBlazor; + +namespace NimbleBlazor.Tests.Acceptance.Shared +{ + /// + /// The MainLayout Component. + /// + public partial class MainLayout + { + private Theme Theme { get; set; } = Theme.Light; + + public ErrorBoundary? ErrorBoundary { get; set; } + + [Inject] + public IJSRuntime? JSRuntime { get; set; } + + protected override void OnParametersSet() + { + ErrorBoundary?.Recover(); + } + } +} \ No newline at end of file diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor.css b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor.css new file mode 100644 index 0000000000..9c95a98781 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Shared/MainLayout.razor.css @@ -0,0 +1,6 @@ +.root { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--ni-nimble-application-background-color); +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/SharedPlaywrightCollectionDefinition.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/SharedPlaywrightCollectionDefinition.cs new file mode 100644 index 0000000000..9da0d17cf0 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/SharedPlaywrightCollectionDefinition.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace NimbleBlazor.Tests.Acceptance +{ + [CollectionDefinition(nameof(PlaywrightFixture))] + public class SharedPlaywrightCollectionDefinition : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Startup.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Startup.cs new file mode 100644 index 0000000000..7f518500a0 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Startup.cs @@ -0,0 +1,32 @@ +namespace NimbleBlazor.Tests.Acceptance; + +/// +/// Web server initialization for Blazor Server +/// +public sealed class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(); + services.AddServerSideBlazor(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + } +} \ No newline at end of file diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/AcceptanceTestsBase.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/AcceptanceTestsBase.cs new file mode 100644 index 0000000000..ea367980c1 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/AcceptanceTestsBase.cs @@ -0,0 +1,62 @@ +using Microsoft.Playwright; +using Xunit; + +namespace NimbleBlazor.Tests.Acceptance +{ + [Collection(nameof(PlaywrightFixture))] + public abstract class AcceptanceTestsBase : IClassFixture + { + private PlaywrightFixture _playwrightFixture; + private readonly BlazorServerWebHostFixture _blazorServerClassFixture; + + protected AcceptanceTestsBase( + PlaywrightFixture playwrightFixture, + BlazorServerWebHostFixture blazorServerClassFixture) + { + _playwrightFixture = playwrightFixture; + _blazorServerClassFixture = blazorServerClassFixture; + } + + private IBrowserContext BrowserContext + { + get + { + return _playwrightFixture.BrowserContext!; + } + } + + protected async Task NewPageForRouteAsync(string route) + { + var page = await BrowserContext.NewPageAsync(); + await NavigateToPageAsync(page, route); + await WaitForNimbleBlazorInitializationAsync(page); + return new AsyncDisposablePage(page); + } + + private async Task NavigateToPageAsync(IPage page, string route) + { + var address = new Uri(_blazorServerClassFixture.ServerAddress!, route).AbsoluteUri; + await page.GotoAsync(address); + } + + private async Task WaitForNimbleBlazorInitializationAsync(IPage page) + { + await page.WaitForFunctionAsync("window.NimbleBlazor && window.NimbleBlazor.calledAfterStarted === true"); + } + + protected sealed class AsyncDisposablePage : IAsyncDisposable + { + public IPage Page { get; private set; } + + public AsyncDisposablePage(IPage page) + { + Page = page; + } + + public async ValueTask DisposeAsync() + { + await Page.CloseAsync(); + } + } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/DialogTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/DialogTests.cs new file mode 100644 index 0000000000..a904d818a5 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/DialogTests.cs @@ -0,0 +1,37 @@ +using Microsoft.Playwright; +using Xunit; + +namespace NimbleBlazor.Tests.Acceptance +{ + public class DialogTests : AcceptanceTestsBase + { + public DialogTests(PlaywrightFixture playwrightFixture, BlazorServerWebHostFixture blazorServerClassFixture) + : base(playwrightFixture, blazorServerClassFixture) + { + } + + [Fact] + public async Task Dialog_CanOpenAndCloseAsync() + { + await using (var pageWrapper = await NewPageForRouteAsync("DialogOpenAndClose")) + { + var page = pageWrapper.Page; + var openButton = page.Locator("nimble-button", new PageLocatorOptions() { HasText = "Open" }); + await openButton.ClickAsync(); + + var dialog = page.Locator("nimble-dialog"); + var innerDialog = dialog.GetByRole(AriaRole.Dialog); + await Assertions.Expect(innerDialog).ToBeVisibleAsync(); + await Assertions.Expect(dialog).ToContainTextAsync("Example Dialog"); + + var closeButton = page.Locator("nimble-button", new PageLocatorOptions() { HasText = "Close" }); + await closeButton.ClickAsync(); + + await Assertions.Expect(innerDialog).Not.ToBeVisibleAsync(); + + var textField = page.Locator("nimble-text-field"); + await Assertions.Expect(textField).ToHaveAttributeAsync("current-value", "Custom Close Reason"); + } + } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/DrawerTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/DrawerTests.cs new file mode 100644 index 0000000000..b6598b1c7e --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/DrawerTests.cs @@ -0,0 +1,37 @@ +using Microsoft.Playwright; +using Xunit; + +namespace NimbleBlazor.Tests.Acceptance +{ + public class DrawerTests : AcceptanceTestsBase + { + public DrawerTests(PlaywrightFixture playwrightFixture, BlazorServerWebHostFixture blazorServerClassFixture) + : base(playwrightFixture, blazorServerClassFixture) + { + } + + [Fact] + public async Task Drawer_CanOpenAndCloseAsync() + { + await using (var pageWrapper = await NewPageForRouteAsync("DrawerOpenAndClose")) + { + var page = pageWrapper.Page; + var openButton = page.Locator("nimble-button", new PageLocatorOptions() { HasText = "Open" }); + await openButton.ClickAsync(); + + var drawer = page.Locator("nimble-drawer"); + var drawerInnerDialog = drawer.GetByRole(AriaRole.Dialog); + await Assertions.Expect(drawerInnerDialog).ToBeVisibleAsync(); + await Assertions.Expect(drawer).ToContainTextAsync("Example Drawer"); + + var closeButton = page.Locator("nimble-button", new PageLocatorOptions() { HasText = "Close" }); + await closeButton.ClickAsync(); + + await Assertions.Expect(drawerInnerDialog).Not.ToBeVisibleAsync(); + + var textField = page.Locator("nimble-text-field"); + await Assertions.Expect(textField).ToHaveAttributeAsync("current-value", "Custom Close Reason"); + } + } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/TableTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/TableTests.cs new file mode 100644 index 0000000000..d7b3a59ac3 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/TableTests.cs @@ -0,0 +1,28 @@ +using Microsoft.Playwright; +using Xunit; + +namespace NimbleBlazor.Tests.Acceptance +{ + public class TableTests : AcceptanceTestsBase + { + public TableTests(PlaywrightFixture playwrightFixture, BlazorServerWebHostFixture blazorServerClassFixture) + : base(playwrightFixture, blazorServerClassFixture) + { + } + + [Fact] + public async Task Table_RendersBoundDataAsync() + { + await using (var pageWrapper = await NewPageForRouteAsync("TableBindToData")) + { + var page = pageWrapper.Page; + var table = page.Locator("nimble-table"); + await Assertions.Expect(table).ToBeVisibleAsync(); + + var rows = table.Locator("nimble-table-row"); + await Assertions.Expect(rows).ToHaveCountAsync(5); + await Assertions.Expect(rows).ToContainTextAsync(new string[] { "A0", "A1", "A2", "A3", "A4" }); + } + } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/WebHostServerFixture.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/WebHostServerFixture.cs new file mode 100644 index 0000000000..f4a209509d --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/WebHostServerFixture.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Xunit; + +namespace NimbleBlazor.Tests.Acceptance; + +public abstract class WebHostServerFixture : IAsyncLifetime, IDisposable +{ + private IHost? _host; + + public Uri? ServerAddress { get; set; } + + public async Task InitializeAsync() + { + _host = CreateWebHost(); + await _host.StartAsync(); + + var server = _host.Services.GetRequiredService(); + var addressFeature = server.Features.Get(); + ServerAddress = new Uri(addressFeature!.Addresses.First()); + } + + public async Task DisposeAsync() + { + if (_host != null) + { + await _host.StopAsync(); + _host.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _host?.Dispose(); + } + } + + protected abstract IHost CreateWebHost(); +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/_Imports.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/_Imports.razor new file mode 100644 index 0000000000..27670ad173 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/_Imports.razor @@ -0,0 +1,11 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using NimbleBlazor.Tests.Acceptance +@using NimbleBlazor.Tests.Acceptance.Shared +@using NimbleBlazor.Tests.Acceptance.Pages diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/appsettings.Development.json b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/appsettings.Development.json new file mode 100644 index 0000000000..770d3e9314 --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/appsettings.json b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/wwwroot/css/site.css b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/wwwroot/css/site.css new file mode 100644 index 0000000000..3afadc202e --- /dev/null +++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/wwwroot/css/site.css @@ -0,0 +1,28 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/wwwroot/favicon.ico b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/wwwroot/favicon.ico new file mode 100644 index 0000000000..63e859b476 Binary files /dev/null and b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/wwwroot/favicon.ico differ diff --git a/packages/nimble-blazor/build/generate-playwright-version-properties/source/Playwright.PackageVersion.template b/packages/nimble-blazor/build/generate-playwright-version-properties/source/Playwright.PackageVersion.template new file mode 100644 index 0000000000..b09d0ce609 --- /dev/null +++ b/packages/nimble-blazor/build/generate-playwright-version-properties/source/Playwright.PackageVersion.template @@ -0,0 +1,5 @@ + + + PLAYWRIGHT_VERSION_PLACEHOLDER + + diff --git a/packages/nimble-blazor/build/generate-playwright-version-properties/source/index.js b/packages/nimble-blazor/build/generate-playwright-version-properties/source/index.js new file mode 100644 index 0000000000..8d68d37b27 --- /dev/null +++ b/packages/nimble-blazor/build/generate-playwright-version-properties/source/index.js @@ -0,0 +1,16 @@ +const fs = require('fs'); +const path = require('path'); + +const resolvedPlaywrightPackageJsonPath = require.resolve('playwright/package.json'); +const playwrightVersion = JSON.parse(fs.readFileSync(resolvedPlaywrightPackageJsonPath, 'utf8')).version; + +const templatePath = path.resolve(__dirname, 'Playwright.PackageVersion.template'); +const templateContents = fs.readFileSync(templatePath, 'utf8'); +const propsFileContent = templateContents.replace(/PLAYWRIGHT_VERSION_PLACEHOLDER/g, playwrightVersion); + +const destAbsoluteDir = path.resolve(__dirname, '../dist/'); +if (!fs.existsSync(destAbsoluteDir)) { + fs.mkdirSync(destAbsoluteDir, { recursive: true }); +} +const destPath = path.resolve(destAbsoluteDir, 'Playwright.PackageVersion.props'); +fs.writeFileSync(destPath, propsFileContent, 'utf8'); diff --git a/packages/nimble-blazor/package.json b/packages/nimble-blazor/package.json index 8651eeed21..0f499e6865 100644 --- a/packages/nimble-blazor/package.json +++ b/packages/nimble-blazor/package.json @@ -3,6 +3,7 @@ "version": "11.8.30", "description": "Blazor components for the NI Nimble Design System", "scripts": { + "postinstall": "node build/generate-playwright-version-properties/source/index.js", "build": "npm run generate-icons && npm run generate-hybrid && npm run build:release && npm run build:client", "build:release": "dotnet build -c Release /p:TreatWarningsAsErrors=true /warnaserror", "build:client": "dotnet publish -p:BlazorEnableCompression=false -c Release Examples/Demo.Client --output dist/blazor-client-app", @@ -43,6 +44,7 @@ "@ni/eslint-config-javascript": "^4.2.0", "@ni/nimble-components": "*", "@ni/nimble-tokens": "*", + "playwright": "^1.30.0", "@rollup/plugin-node-resolve": "^15.0.1", "cross-env": "^7.0.3", "glob": "^8.1.0",