Stably Logo
Engineering
Sun Dec 08 2024

Cookie Management in Playwright

Learn how to manage browser cookies in Playwright for efficient UI testing and state management.


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.


The Cookie Conundrum

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.


Understanding the Browser Context

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.


The Cookie Management Arsenal

Let's explore the tools Playwright gives us for managing cookies. Imagine these as different specialized tools in your testing toolkit:


Cookie Inspection with browserContext.cookies()

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');


Cookie Cleanup with 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' });

Cookie Injection with 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: '/',
}]);

The Swiss Army Knife: 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:

  1. Implementing test-specific endpoints that let you directly set server state
  2. Using database fixtures or API calls to ensure server state matches your stored browser state
  3. Setting longer session timeouts in your test environment
  4. Adding retry logic for cases where state restoration might fail

Practical Applications

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.

Speed Up Your Test Suite

Imagine you're testing an e-commerce checkout flow. Every test needs a product in the cart, which requires:

  1. Navigating to a product
  2. Selecting options
  3. Clicking "Add to Cart"
  4. Waiting for cart updates

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!

Testing First-Time vs Returning User Experiences

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();
});

A Simpler Solution

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. 🚀

Posted by
Bassam Ahmed
Bassam Ahmed
Software Engineer at Stably
Stably Logo
All systems operational.
Copyright ©2025 stably.ai