Photo by Hunter Haley on Unsplash

Enable Playwright so that you can easily use it (including on your build agents) [Part 2 ]

Xavier Solau
YounitedTech
Published in
6 min readOct 11, 2022

--

Set up Playwright Fixture

Now that we have seen in the previous part of the article how to set up our application and test projects we can prepare the test fixture to use Playwright.

Note that you can find all source code used in this article in this repository!

Create the PlaywrightFixture

Let’s set up a Fixture PlaywrightFixture that will initialize a Playwright context to be used in the E2E tests. The fixture will setup Playwright to run our tests on the Chromium, WebKit, and Firefox rendering engines.

Since our fixture needs asynchronous initialization and disposal it is going to implement the xUnit interface IAsyncLifetime.

/// <summary>
/// Playwright fixture implementing an asynchronous life cycle.
/// </summary>
public class PlaywrightFixture : IAsyncLifetime
{
/// <summary>
/// Initialize the Playwright fixture.
/// </summary>

public async Task InitializeAsync()
{
/* Setup Playwright module and the Browsers */
}
/// <summary>
/// Dispose all fixture resources.
/// </summary>
public async Task DisposeAsync()
{
/* Dispose Playwright module and its resources. */
}
}

The fixture initialize the Playwright context and browsers before the test is executed and dispose all resources once the test is completed.

/// <summary>
/// Playwright module.
/// </summary>
public IPlaywright Playwright { get; private set; }
/// <summary>
/// Chromium lazy initializer.
/// </summary>
public Lazy<Task<IBrowser>> ChromiumBrowser { get; private set; }

/// <summary>
/// Firefox lazy initializer.
/// </summary>
public Lazy<Task<IBrowser>> FirefoxBrowser { get; private set; }

/// <summary>
/// Webkit lazy initializer.
/// </summary>
public Lazy<Task<IBrowser>> WebkitBrowser { get; private set; }
/// <summary>
/// Initialize the Playwright fixture.
/// </summary>
public async Task InitializeAsync()
{
// Install Playwright and its dependencies.
InstallPlaywright();
// Create Playwright module.
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
// Setup Browser lazy initializers.
ChromiumBrowser = new Lazy<Task<IBrowser>>(
Playwright.Chromium.LaunchAsync());
FirefoxBrowser = new Lazy<Task<IBrowser>>(
Playwright.Firefox.LaunchAsync());
WebkitBrowser = new Lazy<Task<IBrowser>>(
Playwright.Webkit.LaunchAsync());
}
/// <summary>
/// Dispose all Playwright module resources.
/// </summary>
public async Task DisposeAsync()
{
if (Playwright != null)
{
if (ChromiumBrowser != null && ChromiumBrowser.IsValueCreated)
{
var browser = await ChromiumBrowser.Value;
await browser.DisposeAsync();
}
if (FirefoxBrowser != null && FirefoxBrowser.IsValueCreated)
{
var browser = await FirefoxBrowser.Value;
await browser.DisposeAsync();
}
if (WebkitBrowser != null && WebkitBrowser.IsValueCreated)
{
var browser = await WebkitBrowser.Value;
await browser.DisposeAsync();
}
Playwright.Dispose();
Playwright = null;
}
}

Note that the Browser can be launched with a BrowserTypeLaunchOptions parameter where you can specify among other options to display the browser while running the tests (using Headless = false for instance).

As you can see in the InitializeAsync method, in addition of creating all resources, we also need to call a InstallPlaywright method. This is used in order to deploy all Playwright binaries it may need if not installed yet. This is really useful especially in the case where you would like to run your tests on a build agent for instance.

/// <summary>
/// Install and deploy all binaries Playwright may need.
/// </summary>
private static void InstallPlaywright()
{
var exitCode = Microsoft.Playwright.Program.Main(
new[] { "install-deps" });
if (exitCode != 0)
{
throw new Exception(
$"Playwright exited with code {exitCode} on install-deps");
}
exitCode = Microsoft.Playwright.Program.Main(new[] { "install" });
if (exitCode != 0)
{
throw new Exception(
$"Playwright exited with code {exitCode} on install");
}
}

Note that the first time you run your tests, it may take some additional time to install Playwright and its dependencies.

We are almost done here but in order to complete the Fixture we need to add some additional plumbing code to be able to use our fixture in our xUnit tests.

/// <summary>
/// PlaywrightCollection name that is used in the Collection
/// attribute on each test classes.
/// Like "[Collection(PlaywrightFixture.PlaywrightCollection)]"
/// </summary>
public const string PlaywrightCollection =
nameof(PlaywrightCollection);
[CollectionDefinition(PlaywrightCollection)]
public class PlaywrightCollectionDefinition
: ICollectionFixture<PlaywrightFixture>
{
// This class is just xUnit plumbing code to apply
// [CollectionDefinition] and the ICollectionFixture<>
// interfaces. Witch in our case is parametrized
// with the PlaywrightFixture.
}

Add a GotoPageAsync method

In order to make our test easier we can add some additional code in the fixture.

To identify the browser we want our test cases to run we are going to define a Browser enumeration with the 3 values:

/// <summary>
/// Browser types we can use in the
PlaywrightFixture.
/// </summary>

public enum Browser
{
Chromium,
Firefox,
Webkit,
}

Then we will add a method that actually launch the specified browser and open a web page to the given URL. This way we could centralize and reuse some plumbing code to finally run the actual test logic in the given test handler:

/// <summary>
/// Open a Browser page and navigate to the given URL before
/// applying the given test handler.
/// </summary>
/// <param name="url">URL to navigate to.</param>
/// <param name="testHandler">Test handler to apply on the page.
/// </param>
/// <param name="browserType">The Browser to use to open the page.
/// </param>
/// <returns>The GotoPage task.</returns>

public async Task GotoPageAsync(
string url,
Func<IPage, Task> testHandler,
Browser browserType)
{
// select and launch the browser.
var browser = await SelectBrowserAsync(browserType);
// Create a new context with an option to ignore HTTPS errors.
await using var context = await browser
.NewContextAsync(
new BrowserNewContextOptions
{
IgnoreHTTPSErrors = true
});
// Open a new page.
var page = await context.NewPageAsync();
page.Should().NotBeNull();
try
{
// Navigate to the given URL and wait until loading
// network activity is done.
var gotoResult = await page.GotoAsync(
url,
new PageGotoOptions {
WaitUntil = WaitUntilState.NetworkIdle
});
gotoResult.Should().NotBeNull();
await gotoResult.FinishedAsync();
gotoResult.Ok.Should().BeTrue();
// Run the actual test logic.
await testHandler(page);
}
finally
{

// Make sure the page is closed
await page.CloseAsync();
}
}
/// <summary>
/// Select the IBrowser instance depending on the given browser
/// enumeration value.
/// </summary>
/// <param name="browser">The browser to select.</param>
/// <returns>The selected IBrowser instance.</returns>
private Task<IBrowser> SelectBrowserAsync(Browser browser)
{
return browser switch
{
Browser.Chromium => ChromiumBrowser.Value,
Browser.Firefox => FirefoxBrowser.Value,
Browser.Webkit => WebkitBrowser.Value,
_ => throw new NotImplementedException(),
};
}

Let’s use the fixture

Now that the fixture is ready, we are going to see how we can use it on a test.

First we need a test class that register itself as a PlaywrightFixture user with a xUnit Collection attribute and a constructor with a PlaywrightFixture parameter.

/// <summary>
/// The test class that is using the PlaywrightFixture
/// </summary>
[Collection(PlaywrightFixture.PlaywrightCollection)]
public class MyTestClass
{
private readonly PlaywrightFixture playwrightFixture;
/// <summary>
/// Setup test class injecting a playwrightFixture instance.
/// </summary>
/// <param name="playwrightFixture">The playwrightFixture
/// instance.</param>
public MyTestClass(PlaywrightFixture playwrightFixture)
{
this.playwrightFixture = playwrightFixture;
}
}

Now we can add a test that is going to open a web page on a chromium browser using the fixture and process some test logic to verify some assertions.

[Fact]
public async Task MyFirstTest()
{
var url = "https://my.test.url";
// Open a page and run test logic.
await this.playwrightFixture.GotoPageAsync(
url,
async (page) =>
{
// Apply the test logic on the given page.
},
Browser.Chromium);
}

Note that the Webkit engine doesn’t support WebAssembly on the Windows platform (for now) so its not an issue with Blazor server side but you won’t be able to use it with Blazor WebAssembly.

Although it works perfectly on Linux!

I didn’t notice any limitation with the Chromium or Firefox engine.

--

--