Skip to content

Commit

Permalink
E2E/Acceptance test infrastructure for Nimble Blazor (#1248)
Browse files Browse the repository at this point in the history
# Pull Request

## 🤨 Rationale

#705 (partially addresses - that issue covers both hover/focus testing
and Blazor e2e tests currently)

This PR adds a new test project for Nimble Blazor end-to-end /
acceptance tests. These use Playwright to start up a Chromium browser
instance, connect to a local Blazor Server app with pages using Nimble
Blazor components, and interact with them.

## 👩‍💻 Implementation

Add new project `NimbleBlazor.Tests.Acceptance` to `NimbleBlazor.sln`
(depends on `NimbleBlazor`) (xUnit-based)
- Fixture class `BlazorServerWebHostFixture` spins up a local web server
to host a Blazor Server app.
- This app uses the `all-components-bundle` JS like the other Nimble
Blazor example apps
  - Razor components for the tests are defined in the Pages\ subfolder
- Fixture class `PlaywrightFixture` handles spinning up a Chromium
browser instance to run tests targeting the local Blazor server (uses
Microsoft.Playwright NuGet package)
- Release builds run Chromium headless. Debug builds currently run
headed w/ slow-mo mode to make debugging easier.
- `Tests\AcceptanceTestsBase` (base class for test classes) has logic to
load a new page with Playwright, navigate to the right URL, etc

This PR includes 3 tests: 
- nimble-dialog opening & closing (`DialogTests.cs,
Dialog_CanOpenAndCloseAsync()`, uses `DialogOpenAndClose.razor`)
- nimble-drawer opening & closing (`DrawerTests.cs,
Dialog_CanOpenAndCloseAsync()`, uses `DrawerOpenAndClose.razor`)
- nimble-table binding to data (`TableTests.cs,
Table_RendersBoundDataAsync()`, uses `TableBindToData.razor`).

For future new tests, we should probably aim to exercise the rest of the
code in `NimbleBlazor.lib.module.js`. Dialog/Drawer/Table were chosen to
start with since they each call custom JS methods in that file (that
only had manual test coverage).

Currently each test loads distinct pages / Razor components. Once we
have more tests, it may make sense for some of them to load the same
Razor components if they're testing the same control.

Writing Playwright test code to test Nimble controls is fairly
straightforward, but there's a few quirks. Reading the [SLE E2E Getting
Started with Playwright
Tests](https://dev.azure.com/ni/DevCentral/_git/Skyline?path=/End2EndTests/Getting%20Started%20with%20Playwright%20Tests.md&_a=preview)
doc is a good starting point.

## 🧪 Testing

- Ran new tests locally (in Visual Studio and with `npm run test`)
- Ensured new tests are running as part of the existing `npm run test`
command for Nimble Blazor for the GitHub Actions / CI build.
- i.e. see npm run test output from build log
[here](https://github.com/ni/nimble/actions/runs/4986254518/jobs/8926790926?pr=1248)
```
Test run for /home/runner/work/nimble/nimble/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/bin/Release/net6.0/NimbleBlazor.Tests.Acceptance.dll (.NETCoreApp,Version=v6.0)
Microsoft (R) Test Execution Command Line Tool Version 17.5.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     3, Skipped:     0, Total:     3, Duration: 1 s - NimbleBlazor.Tests.Acceptance.dll (net6.0)
```

## ✅ Checklist

- [x] I have updated the project documentation to reflect my changes or
determined no changes are needed.
  • Loading branch information
msmithNI authored May 19, 2023
1 parent 980eb3c commit 3bcb03f
Show file tree
Hide file tree
Showing 35 changed files with 729 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/nimble-blazor/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
NimbleBlazor/wwwroot/nimble-*/
NimbleBlazor/wwwroot/NimbleBlazor.HybridWorkaround.js
NimbleBlazor/Components/Icons/
build/generate-playwright-version-properties/dist/
artifacts/
bin/
obj/
Expand Down
20 changes: 18 additions & 2 deletions packages/nimble-blazor/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div class="content container">
<p>
Explore the components below to see the Nimble components in action. See the <a
href="https://ni.github.io/nimble/storybook/?path=/story/getting-started--page">Nimble
href="https://nimble.ni.dev/storybook/?path=/docs/getting-started--docs">Nimble
component docs</a> for additional usage details.
</p>
<div class="container">
Expand Down
7 changes: 7 additions & 0 deletions packages/nimble-blazor/NimbleBlazor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace NimbleBlazor.Tests.Acceptance;

/// <summary>
/// Test fixture which starts up a Blazor Server web server
/// </summary>
public class BlazorServerWebHostFixture : WebHostServerFixture
{
protected override IHost CreateWebHost()
{
return new HostBuilder()
.ConfigureWebHost(webHostBuilder => webHostBuilder
.UseKestrel()
.UseStartup<Startup>()
.UseStaticWebAssets()
.UseUrls("http://127.0.0.1:0")) // Pick a port dynamically
.Build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="$(ProjectDir)../../build/generate-playwright-version-properties/dist/Playwright.PackageVersion.props" />

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule)</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule)</NoWarn>
</PropertyGroup>

<ItemGroup>
<AdditionalFiles Include="..\..\CodeAnalysisDictionary.xml" Link="CodeAnalysisDictionary.xml" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Microsoft.Playwright" Version="$(PkgMicrosoft_Playwright_Version)" />
<PackageReference Include="NI.CSharp.Analyzers" Version="2.0.4" />
<PackageReference Include="System.ComponentModel" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.execution" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\NimbleBlazor\NimbleBlazor.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@page "/DialogOpenAndClose"
@namespace NimbleBlazor.Tests.Acceptance.Pages
@inherits LayoutComponentBase

<NimbleButton @onclick="OpenDialogAsync">Open</NimbleButton>

<NimbleDialog TCloseReason="string" @ref="_dialog">
Example Dialog
<NimbleButton @onclick="CloseDialogAsync">Close</NimbleButton>
</NimbleDialog>

<NimbleTextField @bind-Value="DialogCloseReason" @ref="_textField"></NimbleTextField>

@code {
private NimbleDialog<string>? _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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@page "/DrawerOpenAndClose"
@namespace NimbleBlazor.Tests.Acceptance.Pages
@inherits LayoutComponentBase

<NimbleButton @onclick="OpenDrawerAsync">Open</NimbleButton>

<NimbleDrawer TCloseReason="string" @ref="_drawer">
Example Drawer
<NimbleButton @onclick="CloseDrawerAsync">Close</NimbleButton>
</NimbleDrawer>

<NimbleTextField @bind-Value="DrawerCloseReason" @ref="_textField"></NimbleTextField>

@code {
private NimbleDrawer<string>? _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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@page "/TableBindToData"
@namespace NimbleBlazor.Tests.Acceptance.Pages
@using System.Diagnostics.CodeAnalysis;
@inherits LayoutComponentBase

<NimbleTable IdFieldName="Id" @bind-Data="TableData">
<NimbleTableColumnText FieldName="Field1" ColumnId="1">Column 1</NimbleTableColumnText>
</NimbleTable>

@code {
[NotNull]
public IEnumerable<RowData> TableData { get; set; } = Enumerable.Empty<RowData>();

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; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@page "/"
@namespace NimbleBlazor.Tests.Acceptance.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "_Layout";
}

<component type="typeof(App)" render-mode="ServerPrerendered" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@using Microsoft.AspNetCore.Components.Web
@namespace NimbleBlazor.Tests.Acceptance.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/" />
<link href="css/site.css" rel="stylesheet" />
<link href="_content/NimbleBlazor/nimble-tokens/css/fonts.css" rel="stylesheet" />
<link href="NimbleBlazor.Tests.Acceptance.styles.css" rel="stylesheet" />
<script src="_content/NimbleBlazor/nimble-components/all-components-bundle.min.js"></script>
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@RenderBody()

<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

<script src="_framework/blazor.server.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Microsoft.Playwright;
using Xunit;
using PlaywrightProgram = Microsoft.Playwright.Program;

namespace NimbleBlazor.Tests.Acceptance;

/// <summary>
/// Fixture to handle Playwright initialization for acceptance tests.
/// </summary>
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();
}
}
Loading

0 comments on commit 3bcb03f

Please sign in to comment.