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:
login.setup.tsruns first- Logs in and saves session →
storageState.json - All test files reuse this session
- 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
.envfor credentials - Add API login for speed
- Rotate test users (avoid blocking)
- Keep session fresh (auto re-login if expired)