Playwright Guide
Overview
Playwright is used for end-to-end (E2E) testing in humuus, simulating complete user flows in real browser environments. These tests validate the application from a user's perspective across multiple browsers.
When to Use Playwright
- Complete User Flows: Testing entire workflows like login, workshop creation, and participation
- Cross-Browser Testing: Ensuring compatibility with Chrome, Firefox, and Safari
- UI Interaction Testing: Validating that buttons, forms, and navigation work correctly
- Integration Testing: Testing how frontend and backend work together in realistic scenarios
Setup
Playwright is configured to run automatically through GitHub Actions. Tests simulate user interactions in isolated browser environments.
Basic Test Structure
import { test, expect } from '@playwright/test';
test('test description', async ({ page }) => {
// Navigate to page
await page.goto('http://localhost:3000/');
// Interact with elements
await page.getByRole('button', { name: 'Click Me' }).click();
// Assert expected behavior
await expect(page.getByText('Success')).toBeVisible();
});
Navigating and Interacting
Navigation
// Navigate to URL
await page.goto('http://localhost:3000/');
// Navigate with wait options
await page.goto('http://localhost:3000/', {
waitUntil: 'networkidle'
});
// Go back/forward
await page.goBack();
await page.goForward();
// Reload page
await page.reload();
Clicking Elements
// Click by role and name
await page.getByRole('button', { name: 'Submit' }).click();
// Click by text
await page.getByText('Click here').click();
// Click by test ID
await page.getByTestId('submit-button').click();
// Double click
await page.getByRole('button', { name: 'Edit' }).dblclick();
// Right click
await page.getByRole('button', { name: 'Options' }).click({ button: 'right' });
Filling Forms
// Fill text input
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
// Fill and press key
await page.getByRole('textbox', { name: 'Password' }).fill('pass123');
await page.getByRole('textbox', { name: 'Password' }).press('Enter');
// Select from dropdown
await page.getByRole('combobox', { name: 'Country' }).selectOption('Germany');
// Check checkbox
await page.getByRole('checkbox', { name: 'Accept terms' }).check();
// Uncheck checkbox
await page.getByRole('checkbox', { name: 'Newsletter' }).uncheck();
Selecting Elements
// By role
page.getByRole('button', { name: 'Submit' })
page.getByRole('heading', { name: 'Title' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('link', { name: 'Home' })
// By text
page.getByText('Welcome')
page.getByText(/welcome/i) // Case insensitive regex
// By test ID
page.getByTestId('user-profile')
// By label
page.getByLabel('Username')
// By placeholder
page.getByPlaceholder('Enter your name')
// Filter elements
page.getByRole('button').filter({ hasText: 'Start' })
page.getByRole('button').nth(2) // Third button
page.getByRole('button').first()
page.getByRole('button').last()
Assertions
Visibility Assertions
// Element is visible
await expect(page.getByText('Welcome')).toBeVisible();
// Element is hidden
await expect(page.getByText('Error')).toBeHidden();
// Element is attached to DOM
await expect(page.getByTestId('content')).toBeAttached();
Content Assertions
// Text content
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByTestId('message')).toContainText('Success');
// Count elements
await expect(page.getByRole('listitem')).toHaveCount(5);
// Attribute values
await expect(page.getByRole('link')).toHaveAttribute('href', '/home');
// CSS classes
await expect(page.getByTestId('alert')).toHaveClass('error');
State Assertions
// Input value
await expect(page.getByRole('textbox')).toHaveValue('test@example.com');
// Checkbox/radio state
await expect(page.getByRole('checkbox')).toBeChecked();
await expect(page.getByRole('checkbox')).not.toBeChecked();
// Button state
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('button')).toBeDisabled();
URL Assertions
// Current URL
await expect(page).toHaveURL('http://localhost:3000/dashboard');
await expect(page).toHaveURL(/dashboard/);
// Page title
await expect(page).toHaveTitle('humuus - Dashboard');
🚀 Automated Test Generation with Playwright Codegen
To enhance the professionalism of your test generation documentation, I have restructured and refined the language, focusing on clarity, precision, and best practices.
Utilizing Playwright Codegen
The Playwright codegen tool facilitates the rapid creation of test scripts by recording user interactions.
Procedure for Test Generation
-
Execute the following command in your terminal to start the interactive recorder:
pnpm exec playwright codegen -
Perform the desired user scenario within the launched browser instance. Playwright will automatically generate corresponding test code based on your actions.
-
Once the scenario is complete, copy the generated code and paste it into a new test file within your project. Crucially, review and refactor the code to address any extraneous steps or potential mistakes recorded during the interactive session, ensuring the final test script is clean, robust, and maintains best practices.
Extracting Dynamic Workshop Identifiers (Lobby Codes)
When testing multi-view workshops, it is necessary to dynamically extract the workshop's unique lobby code to facilitate subsequent actions or assertions.
Use the following pattern to reliably capture and validate the code:
const lobbyText = await page.getByText(/Teilnehmen auf humuus\.de\s*\|\s*\d+/).innerText();
// Uses a regular expression to extract the numeric code from the text.
const lobbyCode = (lobbyText.match(/\d+/) ?? [''])[0];
expect(lobbyCode).not.toEqual('');
Simulating Multi-User Scenarios
Testing interactions between multiple users requires the simulation of multiple concurrent browser environments. This is achieved by recording each user's perspective separately and managing distinct browser contexts within the test script.
-
Record and generate a separate test file for each user's flow.
-
Modify the test function signature to access the
browserfixture, which is essential for creating new, isolated contexts:
test('User interaction test', async ({ page, browser }) => {
// ... test implementation
});
- To simulate a new user session—for example, to bypass shared login states or cookies—instantiate a new browser context and page. This is the recommended practice for simulating independent users:
// Creates a new, isolated browser context (session)
const browser1 = await browser.newContext();
// Opens a new page within the new context for the second user
const page1 = await browser1.newPage();
For more information about Multi-User Testing please delve into the next chapter.
Multi-User Testing
test('host and guest interaction', async ({ browser }) => {
// Create two separate contexts (users)
const hostContext = await browser.newContext();
const guestContext = await browser.newContext();
const hostPage = await hostContext.newPage();
const guestPage = await guestContext.newPage();
// Host creates workshop
await hostPage.goto('http://localhost:3000/');
// ... host login and workshop creation
// Guest joins workshop
await guestPage.goto('http://localhost:3000/join');
// ... guest joins using code
// Verify both see the same content
await expect(hostPage.getByText('Workshop Started')).toBeVisible();
await expect(guestPage.getByText('Workshop Started')).toBeVisible();
// Cleanup
await hostContext.close();
await guestContext.close();
});
Cross-Browser Testing
// Run test on specific browser
test.use({ browserName: 'chromium' });
test.use({ browserName: 'firefox' });
test.use({ browserName: 'webkit' }); // Safari
// Or configure in playwright.config.ts
export default {
projects: [
{ name: 'Chrome', use: { browserName: 'chromium' } },
{ name: 'Firefox', use: { browserName: 'firefox' } },
{ name: 'Safari', use: { browserName: 'webkit' } },
],
};
Screenshots and Videos
test('capture screenshot on failure', async ({ page }) => {
await page.goto('http://localhost:3000/');
// Take screenshot
await page.screenshot({ path: 'screenshots/homepage.png' });
// Screenshot of specific element
await page.getByRole('button', { name: 'Start' })
.screenshot({ path: 'screenshots/button.png' });
});
// Configure in playwright.config.ts for automatic capture
export default {
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
};
Best Practices
1. Use Meaningful Selectors
// Good - accessible and stable
await page.getByRole('button', { name: 'Submit' });
await page.getByLabel('Email');
// Avoid - fragile
await page.locator('.btn-submit');
await page.locator('div > button:nth-child(3)');
2. Handle Timing Issues
// Let Playwright auto-wait
await expect(page.getByText('Data loaded')).toBeVisible();
// Don't use arbitrary waits
// Bad: await page.waitForTimeout(5000);
3. Test Real User Flows
// Test complete scenarios, not isolated clicks
test('complete checkout process', async ({ page }) => {
// Add item to cart
// Proceed to checkout
// Fill shipping info
// Complete payment
// Verify order confirmation
});
4. Isolate Tests
test.beforeEach(async ({ page }) => {
// Reset to clean state before each test
await page.goto('http://localhost:3000/');
});
5. Test Critical Paths First
- User authentication
- Workshop creation and joining
- Core quiz functionality
- Navigation between workshop elements
Running Tests
# Run all tests
pnpm exec playwright test
# Run specific test file
pnpm exec playwright test tests/quiz.spec.ts
# Run tests with UI
pnpm exec playwright test --ui
# Run tests in specific browser
pnpm exec playwright test --project=firefox
Debugging
// Add breakpoint
await page.pause();
// Console logs
page.on('console', msg => console.log(msg.text()));
// Network requests
page.on('request', request => console.log(request.url()));
Resources
- Playwright Documentation: https://playwright.dev/
- Best Practices: https://playwright.dev/docs/best-practices
- API Reference: https://playwright.dev/docs/api/class-page