Playwright Save Login Session with POM

Here’s a complete, real-world Playwright POM framework showing how to:

  • Login once using beforeAll
  • Share the session across tests
  • Use Page Object Model (POM)
  • Follow best practices for scalability

✅ 1. Project Structure (Recommended)

playwright-project/

├── tests/
│ ├── login.setup.ts # Login once & save session
│ ├── example.spec.ts # Actual test cases

├── pages/
│ ├── LoginPage.ts
│ ├── DashboardPage.ts

├── utils/
│ ├── baseTest.ts # Custom fixture (optional advanced)

├── playwright.config.ts
└── package.json

✅ 2. Login Page (POM)

pages/LoginPage.ts

import { Page } from '@playwright/test';export class LoginPage {
readonly page: Page; constructor(page: Page) {
this.page = page;
} async navigate() {
await this.page.goto('https://example.com/login');
} async login(username: string, password: string) {
await this.page.fill('#username', username);
await this.page.fill('#password', password);
await this.page.click('#loginBtn');
}
}

✅ 3. Dashboard Page (POM)

pages/DashboardPage.ts

import { Page, expect } from '@playwright/test';export class DashboardPage {
readonly page: Page; constructor(page: Page) {
this.page = page;
} async verifyDashboardLoaded() {
await expect(this.page.locator('h1')).toHaveText('Dashboard');
}
}

✅ 4. Login Setup (Runs Once Before All Tests)

👉 This is the key part (beforeAll session handling)

tests/login.setup.ts

import { test as setup } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';setup('Login and save session', async ({ page }) => {
const loginPage = new LoginPage(page); await loginPage.navigate();
await loginPage.login('testuser', 'password123'); // Save session
await page.context().storageState({ path: 'storageState.json' });
});

✅ 5. Playwright Config (Use Stored Session)

playwright.config.ts

import { defineConfig } from '@playwright/test';export default defineConfig({
testDir: './tests', use: {
baseURL: 'https://example.com',
storageState: 'storageState.json', // reuse login session
headless: false
}, projects: [
{
name: 'setup',
testMatch: /login\.setup\.ts/,
},
{
name: 'tests',
dependencies: ['setup'], // ensures login runs first
testMatch: /.*\.spec\.ts/,
},
],
});

✅ 6. Test File (Uses Shared Session)

tests/example.spec.ts

import { test } from '@playwright/test';
import { DashboardPage } from '../pages/DashboardPage';test.describe('Dashboard Tests', () => { test.beforeAll(async () => {
console.log('Runs once before all tests');
}); test('Verify dashboard page', async ({ page }) => {
const dashboard = new DashboardPage(page); await page.goto('/dashboard');
await dashboard.verifyDashboardLoaded();
}); test('Another test using same session', async ({ page }) => {
await page.goto('/dashboard/settings');
console.log('User is already logged in');
});});

✅ 7. How This Works (Important)

🔹 Flow:

  1. login.setup.ts runs first
  2. Logs in and saves session → storageState.json
  3. All test files reuse this session
  4. No repeated login → faster execution

✅ 8. Alternative: Using beforeAll Without storageState (Not Recommended)

test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage(); await page.goto('/login');
await page.fill('#username', 'user');
await page.fill('#password', 'pass');
await page.click('#loginBtn'); // ❌ This session is NOT shared across tests automatically
});

👉 Problem:

  • Each test gets a new context
  • Session is NOT reused

✅ 9. Best Practice Summary

✔ Use storageState (Recommended)
✔ Keep login logic in setup file
✔ Use POM for maintainability
✔ Use project dependencies
✔ Avoid login in every test


✅ 10. Pro Tips (Real Project)

  • Store credentials in .env
  • Use multiple storage states for multi-users
  • Use fixtures for advanced reuse
  • Add retry logic for flaky login

Here’s a complete, real-world Playwright setup for multi-user session handling (Admin + User) using POM + storageState + project dependencies.

This is how teams handle role-based testing at scale.


✅ 1. Goal

We want to:

  • Login as Admin
  • Login as Normal User
  • Save both sessions separately
  • Run tests using either role without logging in again

✅ 2. Project Structure

playwright-project/

├── tests/
│ ├── setup/
│ │ ├── admin.setup.ts
│ │ ├── user.setup.ts
│ │
│ ├── admin.spec.ts
│ ├── user.spec.ts

├── pages/
│ ├── LoginPage.ts
│ ├── DashboardPage.ts

├── storage/
│ ├── adminState.json
│ ├── userState.json

├── playwright.config.ts

✅ 3. Login Page (Reusable POM)

pages/LoginPage.ts

import { Page } from '@playwright/test';export class LoginPage {
constructor(private page: Page) {} async navigate() {
await this.page.goto('/login');
} async login(username: string, password: string) {
await this.page.fill('#username', username);
await this.page.fill('#password', password);
await this.page.click('#loginBtn');
}
}

✅ 4. Admin Login Setup

tests/setup/admin.setup.ts

import { test as setup } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';setup('Login as Admin', async ({ page }) => {
const login = new LoginPage(page); await login.navigate();
await login.login('adminUser', 'adminPass'); await page.context().storageState({
path: 'storage/adminState.json',
});
});

✅ 5. User Login Setup

tests/setup/user.setup.ts

import { test as setup } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';setup('Login as User', async ({ page }) => {
const login = new LoginPage(page); await login.navigate();
await login.login('normalUser', 'userPass'); await page.context().storageState({
path: 'storage/userState.json',
});
});

✅ 6. Playwright Config (Multi-User Projects)

playwright.config.ts

import { defineConfig } from '@playwright/test';export default defineConfig({
testDir: './tests', use: {
baseURL: 'https://example.com',
headless: false,
}, projects: [
// 🔹 Admin Setup
{
name: 'admin-setup',
testMatch: /admin\.setup\.ts/,
}, // 🔹 User Setup
{
name: 'user-setup',
testMatch: /user\.setup\.ts/,
}, // 🔹 Admin Tests
{
name: 'admin-tests',
dependencies: ['admin-setup'],
use: {
storageState: 'storage/adminState.json',
},
testMatch: /admin\.spec\.ts/,
}, // 🔹 User Tests
{
name: 'user-tests',
dependencies: ['user-setup'],
use: {
storageState: 'storage/userState.json',
},
testMatch: /user\.spec\.ts/,
},
],
});

✅ 7. Admin Test Example

tests/admin.spec.ts

import { test, expect } from '@playwright/test';test('Admin can access admin panel', async ({ page }) => {
await page.goto('/admin'); await expect(page.locator('h1')).toHaveText('Admin Dashboard');
});

✅ 8. User Test Example

tests/user.spec.ts

import { test, expect } from '@playwright/test';test('User cannot access admin panel', async ({ page }) => {
await page.goto('/admin'); await expect(page.locator('text=Access Denied')).toBeVisible();
});

✅ 9. Advanced: Use Fixtures (Cleaner Approach)

👉 Create a reusable fixture for roles

utils/fixtures.ts

import { test as base } from '@playwright/test';export const test = base.extend<{
adminPage: any;
userPage: any;
}>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'storage/adminState.json',
});
const page = await context.newPage();
await use(page);
await context.close();
}, userPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'storage/userState.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});

Use Fixture in Test

import { test } from '../utils/fixtures';test('Admin vs User behavior', async ({ adminPage, userPage }) => {
await adminPage.goto('/dashboard');
await userPage.goto('/dashboard'); console.log('Both sessions running in parallel');
});

✅ 10. Key Benefits

✔ No repeated login
✔ Faster test execution
✔ Role-based validation
✔ Parallel execution ready
✔ Clean separation of concerns


🚀 Real-World Use Cases

  • Admin vs Customer flows
  • RBAC (Role-Based Access Control) testing
  • Multi-tenant apps
  • Permission validation

⚠️ Common Mistakes

❌ Using same storageState for all users
❌ Logging in inside every test
❌ Hardcoding credentials
❌ Not using project dependencies


✅ Pro Tips

  • Use .env for credentials
  • Add API login for speed
  • Rotate test users (avoid blocking)
  • Keep session fresh (auto re-login if expired)

Leave a Comment