If you are currently trying to test pages behind authentication while using Playwright for .NET, you’re in the right place! This post stems from inner frustrations of sparse documentation that has code snippets but no real guidance on where those snippets go or how the feature event works. Can you really save an authenticated state and use it in other tests using Playwright for .NET? YES!
So… let’s see how we are doing this here at NimblePros!
Note: This blog post was written with reference to Microsoft.Playwright.NUnit version 1.43.
What is that .auth folder?
While looking at the Playwright for .NET Authentication page, I noticed that they called out to create a playwright/.auth
folder and add it to your .gitignore
file. I was curious to see if they would save session state to the folder automatically. However, the more I tinkered with how this might work, the more I realized that this isn’t really needed. Because… (the next question explains the “why”…)
But wait… where does my storage state get stored?
When you run your tests, it will look in the Path
that you specify in context.StorageStateAsync()
’s options. Keep in mind that if you follow their documentation and use a Path
value of state.json
, it will store the state.json
in the current working directory.
For example, suppose my tests are in C:\code\SuperSecretProject\tests\SuperSecretProject.FrontEndTests
. When I run my tests in Debug mode, I will find the state.json file in the C:\code\SuperSecretProject\tests\SuperSecretProject.FrontEndTests\bin\Debug\netX.0
folder of your tests project - where netX.0
matches the project’s targeted .NET version.
What kind of data is stored?
Here’s a glimpse at what’s stored in state.json
:
{
"cookies":[
{
"name":".AspNetCore.Antiforgery.TokenCode",
"value":"AntiforgeryValue",
"domain":"localhost",
"path":"/",
"expires":-1,
"httpOnly":true,
"secure":false,
"sameSite":"Strict"
},
// A bunch of marketing cookies
{
"name":"ProjectAuth",
"value":"AuthValue",
"domain":"localhost",
"path":"/",
"expires":-1,
"httpOnly":true,
"secure":true,"
sameSite":"Lax"
}
],
"origins":[]
}
All these code snippets - where do they go?
I’m glad you asked, because they don’t really get into this in the documentation. That’s why I’m blogging this.
Cookies and local storage values can be stored in a file through the help of the BrowserContext.StorageStateAsync() method.
The documentation shows snippets for saving state and creating a new context with saved state. But they are snippets of syntax - not snippets with context.
Before we get into the snippets, let’s lay out some of our classes and how they relate.
Or for those who aren’t familiar with reading UML:
LoginPage
inherits fromBasePageObject
.BasePageObject
is a class we use for all page objects that contains the Base URI for testing.AuthenticatedPlaywrightDriver
depends onLoginPage
. This is an intentional tight coupling, as we need anIPage
to navigate and get our authenication details.LoginPage
fits the bill.AuthenticatedPlaywrightDriver
has many private functions. TheCreateAuthenticatedPage()
is marked asinternal
and can be used by the assembly.- The
AuthenticatedPageBaseTest
is the base class for tests for pages behind authentication. It inherits from thePageTest
class provided by Playwright. TheAuthenticatedPageBaseTest
has a property that is aAuthenticatedPlaywrightDriver
. PageBehindAuthenticationPageTest
inherits fromAuthenticatedPageBaseTest
.
So imagine you have a SuperSecretProject
with a page to view related resources that only Admins can see. You want to write an automated test to ensure it works correctly:
- You need a way to either load the session state from file or create the file if it doesn’t exist.
- You need a login page to go through the steps of logging into the site and save the state to the state file.
- You want to navigate using a page with a browser context that has the saved session state.
- You need a page object to represent your
SuperSecretAdminPage
. This is the equivalent ofPageBehindAuthenticationPageTest
above.
How do we create a new browser context?
Their snippets of code looks like this:
Save Storage State
// Save storage state into the file.
await context.StorageStateAsync(new()
{
Path = "state.json"
});
Create Context with Storage State
// Create a new context with the saved storage state.
var context = await browser.NewContextAsync(new()
{
StorageStatePath = "state.json"
});
But where do these snippets go? Playwright uses browser contexts to run the tests. So this is the flow I’ve gone with:
From what I’ve seen in the documentation, it doesn’t appear to be something that can be set on the default browser. So we are creating our context in AuthenticatedPlaywrightDriver
’s CreateAuthenticatedContext()
:
private async Task<IBrowserContext> CreateAuthenticatedContext()
{
// Load storage state from file
var storageState = await LoadStorageStateFromFile("state.json");
// Create a new browser context with the loaded storage state
var contextOptions = new BrowserNewContextOptions
{
StorageState = storageState.ToString()
};
_context = await _browser!.NewContextAsync(contextOptions);
if (string.IsNullOrEmpty(storageState))
{
// If the state file didn't exist, a new file is created
// That would get you here to this point
// Since the login page object handles logging into the portal,
// We will add that dependency here.
var loginPage = new LoginPage(await _context.NewPageAsync());
await loginPage.LoginAsync();
}
return _context;
}
Our LoadStorageStateFromFile()
method isn’t anything special:
private async Task<string> LoadStorageStateFromFile(string filePath)
{
if (!File.Exists(filePath))
{
// Create the file and release the lock
File.Create(filePath).Dispose();
}
// Read the storage state file
return await File.ReadAllTextAsync(filePath);
}
Where do we save the browser context?
As for LoginAsync()
, it navigates the login page, enters in credentials, and then calls the Save snippet to save the value to the state.json
file. This could be entering credentials in textboxes, clicking links for SSO, or whatever steps you need to take for authentication. Once the authentication is done, then you call the snippet of code to save the state.
How do we use this with Multiple Pages?
All pages behind authentication can follow the pattern that PageBehindAuthenticationPageTest
does:
- Inherit from
AuthenticatedPageBaseTest
. - Use the
AuthenticatedPage
as the IPage object for your tests. This is what contains the storage state.
What’s in the AuthenticatedPageBaseTest?
The authenticated page base test file looks like this:
using Microsoft.Playwright;
namespace NimblePros.SuperSecretProject.FrontEndTests.Tests.Auth;
internal class AuthenticatedPageBaseTest : PageTest
{
private AuthenticatedPlaywrightDriver driver = new AuthenticatedPlaywrightDriver();
public IPage AuthenticatedPage;
[SetUp]
public async Task Setup()
{
AuthenticatedPage = await driver.CreateAuthenticatedPage();
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
driver.Dispose();
}
}
What’s the AuthenticatedPlaywrightDriver?
This is how we launch our Playwright, launch the browser, create the context, and create a new page with the authentication. You can find the code here in this gist for AuthenticatedPlaywrightDriver.cs.
What if my Authentication Token is in Session Storage?
This post focuses on local storage and cookies. However, we also have a post on Sharing Session Storage in Playwright for .NET. That post will walk you through their snippets for accessing session storage.
Conclusion
I hope this blog post has helped enlighten you on how to save state and reuse it in Playwright for .NET.
If you are getting started with Playwright for .NET, we have a couple on-demand webinars that may help you as well:
- Design Patterns for Testing - This includes the Page Object pattern for front-end testing.
- Testing Blazor with Playwright
If you are interested in having us train your devs on testing or working with Playwright for .NET, contact us today!