Ever found yourself wrestling with browser cookies during UI testing? You're not alone. Today, we'll dive into how Playwright - Microsoft's open-source testing framework - helps us tame these pesky little data crumbs that make or break our web applications.
Before we dive in, let's acknowledge something we all know but often forget: cookies fundamentally change how users experience our websites. Think about it - the first time someone visits your e-commerce site, they see an empty cart and maybe a "Welcome!" popup. Come back later, and suddenly they're greeted by name with their abandoned cart items still waiting. This Jekyll-and-Hyde behavior makes testing particularly interesting (and sometimes frustrating).
Enter Playwright, which gives us fine-grained control over these state-managing morsels. But before we get our hands dirty with code, let's understand our testing playground.
In Playwright, we work with something called a browserContext
- think of it as a fresh Chrome installation on a new computer. It's completely pristine, with no history, no cookies, and no cached files. As Playwright puts it, these are "isolated, non-persistent browser sessions." This isolation is fantastic for testing because it means no test can accidentally influence another through leftover state.
Let's explore the tools Playwright gives us for managing cookies. Imagine these as different specialized tools in your testing toolkit:
This method is your cookie detective. Want to know what cookies are currently set? Just call:
const cookies = await context.cookies();
// Optionally, specify URLs to get cookies for specific domains
const googleCookies = await context.cookies('https://google.com');
browserContext.clearCookies()
Think of this as your cookie vacuum cleaner. Need a fresh start? Just:
await context.clearCookies();
// Or clear specific cookies based on name, domain, path, etc.
await context.clearCookies({ name: 'session_id' });
browserContext.addCookies()
This is your cookie factory. Need to simulate a logged-in state? No problem:
await context.addCookies([{
name: 'sessionId',
value: 'abc123',
domain: '.example.com',
path: '/',
}]);
browserContext.storageState()
This is perhaps the most powerful tool in our arsenal. It captures not just cookies, but also localStorage and other browser-side state:
// Save the current state to a file
await context.storageState({ path: 'state.json' });
// Later, create a new context with this state
const context = await browser.newContext({
storageState: 'state.json'
});
However, it's crucial to understand that storageState
isn't actually a complete snapshot of application state. Think of it like taking a photo of the visible part of an iceberg – there's often much more lurking beneath the surface. Modern web applications maintain state in multiple places, and storageState
only captures what's stored in the browser itself.
For example, imagine you're testing an e-commerce site where adding items to a cart creates both client-side cookies and server-side session data. Even if you capture and restore the cookie that identifies the cart, the server might have cleaned up the abandoned cart data, invalidated the session, or applied business logic that makes the stored state invalid.
Here's a concrete example of where this limitation might bite you:
// This might not work as expected
async function captureLoggedInState() {
const context = await browser.newContext();
const page = await context.newPage();
// Log in and save state
await page.goto('https://example.com/login');
await page.fill('#email', '[email protected]');
await page.fill('#password', 'password123');
await page.click('#login-button');
// Save the state, including the session cookie
await context.storageState({ path: 'logged-in-state.json' });
}
// Later...
test('user profile access', async () => {
// Even though we restore the session cookie...
const context = await browser.newContext({
storageState: 'logged-in-state.json'
});
const page = await context.newPage();
// This might still fail if the server has expired the session!
await page.goto('https://example.com/profile');
// 💥 Error: Redirected to login page
});
To make your tests more reliable when dealing with server-dependent state, consider:
Now that we know our tools, let's look at some real-world applications that can make your tests both more efficient and more comprehensive.
Imagine you're testing an e-commerce checkout flow. Every test needs a product in the cart, which requires:
Instead of repeating these steps for every test, try this approach:
// Setup script that runs once
async function captureCartState() {
const browser = await playwright.chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// Add item to cart
await page.goto('<https://shop.example.com/product>');
await page.click('#add-to-cart');
// Save the state
await context.storageState({ path: 'cart-ready-state.json' });
await browser.close();
}
// In your actual tests
const context = await browser.newContext({
storageState: 'cart-ready-state.json'
});
// Your test starts with item already in cart!
Want to ensure your welcome popup only shows once? Here's how:
// Test for first-time users
test('welcome popup shows for new users', async () => {
const context = await browser.newContext(); // Clean state
const page = await context.newPage();
await page.goto('<https://example.com>');
await expect(page.locator('#welcome-popup')).toBeVisible();
});
// Test for returning users
test('welcome popup hidden for returning users', async () => {
const context = await browser.newContext({
storageState: {
cookies: [{
name: 'visited',
value: 'true',
domain: 'example.com',
path: '/'
}]
}
});
const page = await context.newPage();
await page.goto('<https://example.com>');
await expect(page.locator('#welcome-popup')).toBeHidden();
});
While Playwright provides powerful tools for managing browser state, implementing all these patterns robustly requires significant effort and expertise. You'll need to handle edge cases, maintain state fixtures, and potentially create complex setup routines for your tests. At Stably, we've built these capabilities directly into our testing platform, handling all this complexity behind the scenes. Our AI-powered testing solution automatically manages browser state and maintains test reliability without requiring you to write complex state management code. This means you can focus on what matters most - defining the behaviors you want to test, while we handle the intricate details of browser state management.
Want to learn more about how Stably can simplify your testing workflow? Check out our documentation to see how we've made browser state management a breeze, letting you create robust UI tests without getting lost in the technical weeds. 🚀