Photo by Alex Kondratiev on Unsplash

Blazor & Playwright

Mariekie Coetzee
6 min readSep 5, 2023

--

In my earlier blog post, I discussed my insights gained from utilizing BlazingStory. However, it’s equally vital to delve into the testing aspect of the component. In this article, I’ll be detailing my implementation of Playwright as the end-to-end testing framework in a Blazor project.

This project is inspired by the insights shared during one of Microsoft’s community stand-ups. This is the repo that Mackinnon Buck,Microsoft developer, used during the session.

What is Playwright

Playwright is a versatile framework designed for web testing, offering cross-browser web automation capabilities. It empowers automated testing on browsers such as Chromium, Firefox, and WebKit, making it an invaluable tool for ensuring web application compatibility. Developed as an open-source library by Microsoft, Playwright extends its support to multiple programming languages, including Java, Python, C#, and Node.js.

Getting Started

To successfully create and execute our end-to-end tests with Playwright, we must follow a series of implementation steps. It’s worth noting that for this process, I used NUnit. It supports MSTests too.

Here is my repo for reference.

  1. Install playwright package, here is a CLI command :
dotnet add package Microsoft.Playwright.NUnit

2. ⚠️ It is important to build the project after installing the package. This will create the playwright.ps1 which will install playwright CLI. By default Playwright does not install browsers but by building the project it creates the playwright.ps1 script in the bin/debug/netX folder (the x represents the .net version)

dotnet build

3. By running playwright.ps1 it will install the required browsers, it is located in the netx folder (the x represents the .net version):

./bin/Debug/net8.0/playwright.ps1 install

Blazor integration

The referenced project is a WebAssembly project. To enable end-to-end testing, it’s essential to have a server that starts up and hosts the BlazingStory project.

💡 It is important to note that BlazorServer follows a different implementation.

Blazor WebAssembly

Server

As this is just a Proof of Concept (POC), I’ll create a fake Server project, which will only include Program.cs. This minimal setup is necessary for the test to function.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseBlazorFrameworkFiles();

app.UseStaticFiles();

app.UseRouting();

app.MapRazorPages();

app.MapFallbackToFile("index.html");

app.Run();

// Make the implicit Program class public so test projects can access it
public abstract partial class Program
{
}
  • ❗If you are following along, dont forget to add a reference to the WebAssembly Project.

Playwright Infrastructure

The class below is copied from Mackinnon Buck repo .

public class BlazorTest : PageTest
{
protected static readonly Uri RootUri = new("<http://127.0.0.1>");
private readonly WebApplicationFactory<Program> _webApplicationFactory = new();
private HttpClient? _httpClient;

[SetUp]
public async Task BlazorSetup()
{
_httpClient = _webApplicationFactory.CreateClient();
await Context.RouteAsync(
$"{RootUri.AbsoluteUri}**", async route =>
{
var request = route.Request;
var content = request.PostDataBuffer is { } postDataBuffer
? new ByteArrayContent(postDataBuffer)
: null;
var requestMessage = new HttpRequestMessage(new(request.Method), request.Url)
{
Content = content,
};
foreach (var header in request.Headers)
{
requestMessage.Headers.Add(header.Key, header.Value);
}
var response = await _httpClient.SendAsync(requestMessage);
var responseBody = await response.Content.ReadAsByteArrayAsync();
var responseHeaders =
response.Content.Headers.Select(h => KeyValuePair.Create(h.Key, string.Join(",", h.Value)));
await route.FulfillAsync(
new()
{
BodyBytes = responseBody,
Headers = responseHeaders,
Status = (int)response.StatusCode,
}
);
}
);
}
[TearDown]
public void BlazorTearDown()
{
_httpClient?.Dispose();
}
}

Code Review

In the youtube (21:21) he explains that :

  • It extends the PageTest class fixture class in playwright and will start BlazorSetup()& close BlazorTearDown()the app once it completes.
  • The WebApplicationFactory class which is referenced from Microsoft.AspNetCore.Mvc.Testing. It spins up instance of our web application in memory.
private readonly WebApplicationFactory<Program> _webApplicationFactory = new();
...
_httpClient = _webApplicationFactory.CreateClient();

Now we can give it requests without going through the network 🎉 !

Playwright allows to intercepts requests to specific routes. We want to capture any requests going to our base URI

await Context.RouteAsync($"{RootUri.AbsoluteUri}
  • The information is then translated into a format that the _httpClient can understand. The request is then send:
var requestMessage = new HttpRequestMessage(new(request.Method), request.Url)     {        Content = content,     };
  • The response is received
var response = await _httpClient.SendAsync(requestMessage);
var responseBody = await response.Content.ReadAsByteArrayAsync();
var responseHeaders =
response.Content.Headers.Select(h => KeyValuePair.Create(h.Key, string.Join(",", h.Value)));
  • Calling route.FulfillAsync() sends the response back to the browser thru C# code rather than going through the server.

Blazor Server

In the youtube video roughly around 31:24 Mackinnon explains that Blazor Server relies on a WebSocket connection, which is not easily imitated. In this class, it spins up the server as we normally would.

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Playwright.NUnit;

namespace BlazorServerPlaywright.Test.Infrastructure;

public class BlazorTest : PageTest
{
private IHost? _host;

protected Uri RootUri { get; private set; } = default!;

[SetUp]
public async Task SetUpWebApplication()
{
_host = Program.BuildWebHost();

await _host.StartAsync();

RootUri = new(_host.Services.GetRequiredService<IServer>().Features
.GetRequiredFeature<IServerAddressesFeature>()
.Addresses.Single());
}

[TearDown]
public async Task TearDownWebApplication()
{
if (_host is not null)
{
await _host.StopAsync();
_host.Dispose();
}
}
}

Setting up tests

We are now ready to start writing tests, or leverage the codegen feature of playwright and quickly write our tests!

To start, we’ll need to extend our test class with the previously added ‘BlazorTest’ class.

public class QuickgridTest : BlazorTest

Playwright — Codegen

We can utilize a handy code generation feature provided by Playwright — codegen. Make sure you are in your test project folder.

  1. Start by running our web application.
  2. Next, execute the code generation command, directing it to the host where your app is currently running. (⚠️ Be sure to replace ‘localhost’ and the port number with the appropriate values for your setup!)
./bin/Debug/net8.0/playwright.ps1 codegen https://localhost:7297 

This action launches the Playwright Inspector that sets out code depending on the clicks we make.

  • First we need to select NUnit as the library in the inspector window
  • Now we then simulate the user interaction that we want to test and playwright records all the locations for us in the inspector window.
  • ⚠️ It’s crucial to remember to copy the code from the Inspector window to avoid losing this valuable information

In our test, lets paste the code we copied from the inspector window :

[Test]
public async Task MyTest()
{
await Page.GotoAsync("https://localhost:7297/");

await Page.GotoAsync("https://localhost:7297/?path=/docs/buttons--docs");

await Page.GetByRole(AriaRole.Button, new() { Name = "PlaceHolder" }).ClickAsync();

await Page.GetByRole(AriaRole.Link, new() { Name = "Default" }).Nth(1).ClickAsync();

await Page.GetByRole(AriaRole.Row, new() { Name = "NumberOfColumns Set number" }).GetByRole(AriaRole.Button).ClickAsync();

await Page.GetByPlaceholder("Edit number...").ClickAsync();

await Page.GetByPlaceholder("Edit number...").FillAsync("9");

await Page.FrameLocator("iframe").Locator("div").Filter(new() { HasText = "Loading... Heading 0Heading 1Heading 2Heading 3Heading 4Heading 5Heading 6Headin" }).Nth(1).ClickAsync();

}

Lets clean some lines up and also add assertions.

  • We need to navigate to the infrastructure URI thats setup in the BlazorTest.cs class and not hardcode localhost and port number.
// delete - await Page.GotoAsync("https://localhost:7297/");
// delete - await Page.GotoAsync("https://localhost:7297/?path=/docs/buttons--docs");
await Page.GotoAsync(RootUri.AbsoluteUri);
  • Our objective is to ensure that there are precisely 9 columns after the modification. To achieve this, our first step is to locate the elements accurately, rather than relying on the randomly recorded heading text filter.
// delete - await Page.FrameLocator("iframe").Locator("div").Filter(new() { HasText = "Loading... Heading 0Heading 1Heading 2Heading 3Heading 4Heading 5Heading 6Headin" }).Nth(1).ClickAsync();
var app = Page.FrameLocator("iframe").Locator("#app");

var placeHolder = app.Locator("table>thead>tr>th");
  • Now, we can count the number of columns and verify that the correct amount has been added!
var count = await placeHolder.CountAsync();
Assert.That(count, Is.EqualTo(9));

Here is a copy of the complete test :

[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class PlaceholderTest: BlazorTest
{
[Test]
public async Task NumberOfColumns_ShouldIncreaseTo9()
{
await Page.GotoAsync(RootUri.AbsoluteUri);

await Page.GetByRole(AriaRole.Button, new() { Name = "PlaceHolder" }).ClickAsync();

await Page.GetByRole(AriaRole.Link, new() { Name = "Default" }).Nth(1).ClickAsync();

await Page.GetByRole(AriaRole.Row, new() { Name = "NumberOfColumns Set number" }).GetByRole(AriaRole.Button)
.ClickAsync();

await Page.GetByPlaceholder("Edit number...").ClickAsync();

await Page.GetByPlaceholder("Edit number...").FillAsync("9");

var app = Page.FrameLocator("iframe").Locator("#app");

var placeHolder = app.Locator("table>thead>tr>th");
var count = await placeHolder.CountAsync();
Assert.That(count, Is.EqualTo(9));

}
}

The following command will execute the test in Headless mode, opening the browser for us to observe how it replicates user interactions. I included a ‘SlowMo’ parameter to slow down the process slightly, as it would otherwise be too fast to discern the simulation.

dotnet test -- Playwright.LaunchOptions.Headless=false Playwright.LaunchOptions.SlowMo=2000

To debug the test, the following command can be executed. It will launch the inspector and enable you to debug step by step:

PWDEBUG=1 dotnet test --filter "NumberOfColumns_ShouldIncreaseTo9"

Summary

Playwright is a very powerful automation test framework, it makes absolute sense that Microsoft is supporting it. There are many more features available in playwright, i am very grateful for this library as it is making our lives as developers so much easier! Thank you Playwright!

--

--

Mariekie Coetzee

A Software Engineer and gets childlike excited about developing apps. Loves the outdoors, camping and dreaming about the impossible.