One of the first things that you might do with Playwright when you start automating is writing some code to automate logging into your app. It's so incredibly easy to do with npx playwright codegen
as well, and I've demo'ed this to my team as a rudimentary example while introducing Playwright.
As you begin to scale up your tests, you'll likely find that interacting with the login page results in a lot of wasted clicks and network requests. In fact, we found that hitting /auth
in every single one of our tests can cause some unintended side effects. Additionally, logging in repeatedly with the same user credentials doesn't coincide with real-world app usage, so why should we do the same in our tests?
Fixtures are very useful in that they act as a hook to your original tests, similarly to having beforeEach
or afterEach
hooks everywhere. The major upside of fixtures is that it makes for much cleaner and readable code and provides for consistency across all tests.
Here's what a fixture might look like:
import { test as baseTest, type Page } from '@playwright/test'
import { createAuthContext } from './authHelper'
type AuthFixtures = {
mySiteAuth: Page
}
export const test = baseTest.extend<AuthFixtures>({
mySiteAuth: async ({ browser }, use) => {
const { page: authorizedPage, context: authorizedContext } = await createAuthContext(browser)
await use(authorizedPage)
await authorizedContext.close()
}
})
Pretty simple but very important stuff going on here:
- We're retrieving a page and browser context.
- The call to
use
yields back to Playwrighttest
where all your steps are written. - Closing context is important so that browser windows and their pages are closed properly.
You'll want to spend a decent chunk of time figuring out what your function for createAuthContext
might look like, but in the end, you want it to return a new browser context along with a new page for that context. The browser context itself should take advantage of the storageState
that Playwright offers. This storageState
is basically a JSON blob that is written to mimic the exact local storage state of your app. Finally, you'll want to probably add some actual UI login logic if the storage JSON blobs don't exist.
I'll provide a little bit of code below, but keep in mind that this is already well documented by the Playwright team:
export const MY_SITE_FILE = 'playwright/.auth/my_site.json'
export const createAuthContext = async (browser: Browser) => {
const contextOptions: any = { storageState: MY_SITE_FILE }
const context = await browser.newContext(contextOptions)
const page = await context.newPage()
return { page, context }
}
Now, all you'll have to do is change up your test signatures. Most of them probably looked like this:
import { test, expect } from '@playwright/test'
test('do some things', async ({ page }) => {
page.doSomeThings()
})
And now they'll just look like this:
import { test } from './helpers/testFixtures'
import { expect } from '@playwright/test'
test('do some things', async ({ mySiteAuth: page }) => {
page.doTheSameThings()
})
What's awesome here is that you don't need to go and refactor a bunch of individual tests. The most you'll have to do is perhaps remove any calls to your UI login functions, which hopefully is easy to find and remove if you've followed some pretty consistent patterns in your tests.
What I find notable here is the use of await use(authorizedPage)
almost acts like yield
in Ruby, which I can definitely appreciate as someone who's loved the language for so long.
Hopefully this helps explain how to have Playwright fixtures working alongside their authentication patterns. I did feel that Playwright's documentation was a teeny bit lacking in this area. I'll also mention that using this type of approach that decouples from any global setup also allows for extending AuthFixtures
to other sites within the same test suite.
When compared to Selenium WebDriver and how you typically have to set local storage manually through traditional Web APIs, Playwright really does make it a breeze to have already authorized pages at your disposal. Getting right into auth sessions right off the bat really allows for more focused testing.
Top comments (4)
Great article, helped me a lot! 🙏🏼
I'm so glad to hear that! Let me know if you have any follow up questions at all.
I don't understand where is the part where the code sends a request to the server in order to obtain a valid token which should be used throughout the rest of the tests.
Also playwright docs show very different approach from your example here.
playwright.dev/docs/auth
Yes, storage state is very well documented already. You want to have a setup file like
global.setup.ts
that runs in case a storage state doesn't exist. The idea is that once the storage states are saved (and committed) to the Playwright code repo, you can utilize fixtures in order to fetch those states and use them in some custom test fixtures. The fixture is designed to create a custom authenticated page and browser context outside of the usual page and browser context that is provided by Playwright by default.This way, a basic call like
page.goto('https://www.my-site.com/profile')
brings you to an already authenticated browser and all the content you expect as a logged in user should be there.