How To Auth Clerk with Playwright
If you are like me you probably spent a few hours trying to work the two together. Hopefully this tutorial helps you speed up this process
First setup your config to have a dependency. I find this is a better solution that a global setup and teardown, that provides no logs if something goes wrong
projects: [
{
name: 'auth',
testMatch: /global\.auth\.ts/,
use: {
...devices['Desktop Chrome']
},
timeout: 5 * 60 * 1000
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'state.json',
},
teardown: 'teardown',
dependencies: ['auth'],
timeout: 5 * 60 * 1000
},
]
Next Create a user you will use on your github actions flow. This can be done via the Clerk dashboard. I highly recommend creating a basic username + password user. As a lot of OAuth providers restrict tools like playwright from autofilling forms.
Set the ENV variables as follows:
# Playwright
PLAYWRIGHT_E2E_USER_ID="user_***"
PLAYWRIGHT_E2E_USER_EMAIL="name@example.com"
PLAYWRIGHT_E2E_USER_PASSWORD="***"
Next you want to edit your global.auth.ts
import { chromium, expect, test as setup } from '@playwright/test';
import { config as cfg } from '../config';
import Clerk from '@clerk/clerk-js';
import { test } from '@playwright/test';
type ClerkType = typeof Clerk;
const authFile = 'state.json';
setup('Setup Auth', async ({ page, context }) => {
await page.goto("https://your.url")
// some quick check to see if the dom has loaded
const logo = await page.getByRole('img', { name: 'Logo' })
await expect(logo).toBeVisible();
// ENV variables generated using The Clerk Dashboard
const data = {
userId: process.env.PLAYWRIGHT_E2E_USER_ID || '',
loginPayload: {
strategy: 'password',
identifier: process.env.PLAYWRIGHT_E2E_USER_EMAIL || '',
password: process.env.PLAYWRIGHT_E2E_USER_PASSWORD || '',
}
}
//here is where the magic happens
const result = await page.evaluate(async data => {
// wait function as promise
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const wdw = window as Window & typeof globalThis & { Clerk: ClerkType };
/** clear the cookies */
document.cookie.split(";").forEach(function(c) { document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); });
const clerkIsReady = (window: Window & typeof globalThis & { Clerk: ClerkType }) => {
return wdw.Clerk && wdw.Clerk.isReady();
}
while (!clerkIsReady(wdw)) {
await wait(100);
}
/** if the session is still valid just return true */
if (wdw.Clerk.session?.expireAt && wdw.Clerk.session.expireAt > new Date()) {
return true;
}
/** if its a different user currently logged in sign out */
if (wdw.Clerk.user?.id !== data.userId) {
await wdw.Clerk.signOut();
}
/**
* otherwise signin
*/
const res = await wdw.Clerk.client?.signIn.create(data.loginPayload);
if (!res) {
return false
}
/** set the session as active */
await wdw.Clerk.setActive({
session: res.createdSessionId,
});
return true
}, data);
if (!result) {
throw new Error('Failed to sign in');
}
const pageContext = await page.context();
let cookies = await pageContext.cookies();
// clerk polls the session cookie, so we have to set a wait
while (!cookies.some(c => c.name === '__session')) {
cookies = await pageContext.cookies();
}
// store the cookies in the state.json
await pageContext.storageState({ path: authFile });
})
The following code is key to loading the session. Clerk makes a seperate request as part of a handshake after setting the session to Active. This could technically be an infinate loop, however playwright has timeout restrictions if there is an issue requesting the cookie
while (!cookies.some(c => c.name === '__session')) {
cookies = await pageContext.cookies();
}
Thats it! everything should play nice!