| name | playwright-basics |
| description | Core Playwright concepts, setup, and test execution for E2E testing |
Playwright Basics
Playwright is a Node.js library for end-to-end testing of web applications. This guide covers the essentials for testing the Clientt CRM Phoenix application.
Project Structure
The Playwright tests are located in the playwright_tests/ directory at the project root, independent of the Phoenix application:
playwright_tests/
├── tests/
│ ├── features/ # Feature-based tests (BDD scenarios)
│ ├── examples/ # Example test patterns
│ ├── support/ # Helper functions and page objects
│ └── fixtures/ # Custom test fixtures
├── playwright.config.js # Configuration
├── package.json # Dependencies
└── README.md # Documentation
Running Tests
All commands should be run from the playwright_tests directory:
# Run all tests
npm test
# Run with browser UI visible
npm run test:headed
# Interactive UI mode
npm run test:ui
# Debug mode
npm run test:debug
# Run specific test file
npx playwright test tests/features/authentication.spec.js
# Run specific browser
npx playwright test --project=chromium
Basic Test Structure
// @ts-check
const { test, expect } = require('@playwright/test');
test.describe('Feature Name', () => {
test('should do something', async ({ page }) => {
// Navigate to page
await page.goto('/some-path');
// Interact with elements
await page.fill('input[name="email"]', 'test@example.com');
await page.click('button[type="submit"]');
// Wait for navigation/network
await page.waitForLoadState('networkidle');
// Make assertions
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('h1')).toContainText('Welcome');
});
});
Common Playwright Operations
Navigation
// Navigate to URL
await page.goto('/sign-in');
// Navigate with options
await page.goto('/dashboard', { waitUntil: 'networkidle' });
// Go back/forward
await page.goBack();
await page.goForward();
// Reload
await page.reload();
Finding Elements
// CSS selectors
await page.locator('.class-name')
await page.locator('#id')
await page.locator('button[type="submit"]')
// Text content
await page.locator('text=Sign In')
await page.locator('text=/log.*out/i') // Regex
// Data attributes (preferred)
await page.locator('[data-test="submit-button"]')
// Get multiple elements
const items = await page.locator('.list-item').all();
Interactions
// Click
await page.click('button');
// Fill input
await page.fill('input[name="email"]', 'test@example.com');
// Type (with delay between keystrokes)
await page.type('input', 'Hello', { delay: 100 });
// Check/uncheck
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
// Select dropdown
await page.selectOption('select#country', 'USA');
// Upload file
await page.setInputFiles('input[type="file"]', 'path/to/file');
// Keyboard
await page.keyboard.press('Enter');
await page.keyboard.type('Hello World');
Waiting
// Wait for load state
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');
// Wait for selector
await page.waitForSelector('.result');
await page.waitForSelector('.loading', { state: 'hidden' });
// Wait for URL
await page.waitForURL('**/dashboard');
await page.waitForURL(/.*success/);
// Wait for timeout (avoid if possible)
await page.waitForTimeout(1000);
Assertions
const { expect } = require('@playwright/test');
// Page assertions
await expect(page).toHaveURL(/.*dashboard/);
await expect(page).toHaveTitle(/CRM/);
// Element assertions
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('.error')).toBeHidden();
await expect(page.locator('h1')).toContainText('Welcome');
await expect(page.locator('input')).toHaveValue('test@example.com');
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeDisabled();
// Count
await expect(page.locator('.item')).toHaveCount(5);
Configuration
The playwright.config.js file contains:
- baseURL: Default URL for the application (
http://localhost:4002) - timeout: Maximum test execution time (30 seconds)
- projects: Browser configurations (chromium, firefox, webkit)
- webServer: Automatically starts Phoenix server before tests
- use: Shared settings (screenshots, videos, traces)
Environment Variables
Create a .env file in playwright_tests/:
BASE_URL=http://localhost:4002
TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=SecurePassword123!
Access in tests:
const email = process.env.TEST_USER_EMAIL;
Test Hooks
test.describe('Feature', () => {
// Runs once before all tests
test.beforeAll(async () => {
// Setup code
});
// Runs before each test
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
// Runs after each test
test.afterEach(async ({ page }) => {
// Cleanup
});
// Runs once after all tests
test.afterAll(async () => {
// Teardown code
});
test('my test', async ({ page }) => {
// Test code
});
});
Debugging
Debug Mode
npm run test:debug
This opens Playwright Inspector for stepping through tests.
Console Logs
// Listen to console messages
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// Listen to page errors
page.on('pageerror', error => console.log('PAGE ERROR:', error));
Screenshots
// Take screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });
Pause Execution
// Pause in headed mode
await page.pause();
Best Practices
- Use baseURL: Configure in
playwright.config.jsand use relative paths - Wait for network idle: After navigation and form submissions
- Use data-test attributes: More stable than CSS classes
- Explicit waits: Use
waitForSelectorinstead ofwaitForTimeout - Isolate tests: Each test should be independent
- Clean up: Remove test data after tests (or use transactions)
- Use fixtures: For common setup like authentication
Common Patterns
Form Submission
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
Authentication Check
// Check if logged in
const isLoggedIn = !page.url().includes('sign-in');
Multiple Windows/Tabs
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.click('a[target="_blank"]')
]);
await newPage.waitForLoadState();
Troubleshooting
Test Timeouts
- Increase timeout in config:
timeout: 60 * 1000 - Add explicit waits:
await page.waitForLoadState('networkidle')
Element Not Found
- Use Playwright Inspector:
npm run test:debug - Check selector in browser DevTools
- Add waits:
await page.waitForSelector('.element')
Flaky Tests
- Use proper waits (networkidle, selector visible)
- Avoid fixed timeouts
- Check for race conditions
- Ensure test isolation
Resources
- Playwright Documentation
- API Reference
- Best Practices
- Project README:
playwright_tests/README.md