brainstorming
Structured design dialogue that validates ideas before implementation begins.
e2e-test-writer
Write comprehensive end-to-end tests using Playwright with page object
Full skill instructions
This skill provides comprehensive guidance for writing end-to-end tests using Playwright, following best practices including page object model pattern, proper test isolation, and maintainable test architecture.
Initialize Playwright:
# Install Playwright
npm init playwright@latest
# Or add to existing project
npm install -D @playwright/test
npx playwright install
Project Structure:
tests/
├── e2e/
│ ├── auth/
│ ├── features/
│ └── workflows/
├── pages/
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ └── DashboardPage.ts
├── fixtures/
│ ├── test-data.ts
│ └── custom-fixtures.ts
├── utils/
│ ├── helpers.ts
│ └── constants.ts
└── playwright.config.ts
Playwright Configuration:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Deliverable: Playwright project configured and ready
Base Page Class:
// pages/BasePage.ts
import { Page, Locator } from '@playwright/test';
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async goto(path: string) {
await this.page.goto(path);
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
async getTitle(): Promise<string> {
return await this.page.title();
}
async clickElement(locator: Locator) {
await locator.waitFor({ state: 'visible' });
await locator.click();
}
async fillInput(locator: Locator, value: string) {
await locator.waitFor({ state: 'visible' });
await locator.fill(value);
}
async getText(locator: Locator): Promise<string> {
await locator.waitFor({ state: 'visible' });
return await locator.textContent() || '';
}
}
Example Page Object:
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
// Locators
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
readonly signUpLink: Locator;
constructor(page: Page) {
super(page);
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.submitButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error-message');
this.forgotPasswordLink = page.locator('a[href="/forgot-password"]');
this.signUpLink = page.locator('a[href="/signup"]');
}
// Actions
async goto() {
await super.goto('/login');
}
async login(username: string, password: string) {
await this.fillInput(this.usernameInput, username);
await this.fillInput(this.passwordInput, password);
await this.clickElement(this.submitButton);
}
async clickForgotPassword() {
await this.clickElement(this.forgotPasswordLink);
}
async clickSignUp() {
await this.clickElement(this.signUpLink);
}
// Assertions helpers
async getErrorMessage(): Promise<string> {
return await this.getText(this.errorMessage);
}
async isLoginButtonEnabled(): Promise<boolean> {
return await this.submitButton.isEnabled();
}
async waitForErrorMessage() {
await this.errorMessage.waitFor({ state: 'visible' });
}
}
Deliverable: Page objects for all application pages
Basic Test Structure:
// tests/e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { DashboardPage } from '../../pages/DashboardPage';
test.describe('User Login', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login with valid credentials', async ({ page }) => {
await loginPage.login('[email protected]', 'ValidPassword123!');
const dashboardPage = new DashboardPage(page);
await expect(page).toHaveURL('/dashboard');
await expect(dashboardPage.welcomeMessage).toContainText('Welcome back');
});
test('failed login with invalid credentials shows error', async ({ page }) => {
await loginPage.login('[email protected]', 'WrongPassword');
await loginPage.waitForErrorMessage();
const errorMsg = await loginPage.getErrorMessage();
expect(errorMsg).toContain('Invalid username or password');
await expect(page).toHaveURL('/login');
});
test('login button disabled with empty fields', async ({ page }) => {
const isEnabled = await loginPage.isLoginButtonEnabled();
expect(isEnabled).toBe(false);
});
test('forgot password link navigates correctly', async ({ page }) => {
await loginPage.clickForgotPassword();
await expect(page).toHaveURL('/forgot-password');
});
test('sign up link navigates correctly', async ({ page }) => {
await loginPage.clickSignUp();
await expect(page).toHaveURL('/signup');
});
});
Testing User Workflows:
// tests/e2e/workflows/checkout.spec.ts
import { test, expect } from '@playwright/test';
import { ProductPage } from '../../pages/ProductPage';
import { CartPage } from '../../pages/CartPage';
import { CheckoutPage } from '../../pages/CheckoutPage';
test.describe('Checkout Workflow', () => {
test('complete purchase from product to confirmation', async ({ page }) => {
// 1. Browse product
const productPage = new ProductPage(page);
await productPage.goto('/products/item-123');
await expect(productPage.productTitle).toBeVisible();
// 2. Add to cart
await productPage.addToCart();
await expect(productPage.cartBadge).toHaveText('1');
// 3. View cart
await productPage.goToCart();
const cartPage = new CartPage(page);
await expect(cartPage.cartItems).toHaveCount(1);
// 4. Proceed to checkout
await cartPage.proceedToCheckout();
const checkoutPage = new CheckoutPage(page);
// 5. Fill shipping info
await checkoutPage.fillShippingInfo({
name: 'John Doe',
address: '123 Main St',
city: 'San Francisco',
zip: '94102',
country: 'US',
});
// 6. Fill payment info
await checkoutPage.fillPaymentInfo({
cardNumber: '4242424242424242',
expiry: '12/25',
cvv: '123',
});
// 7. Submit order
await checkoutPage.submitOrder();
// 8. Verify confirmation
await expect(page).toHaveURL(/\/order-confirmation/);
await expect(page.locator('.success-message')).toContainText('Order placed successfully');
});
});
Deliverable: Comprehensive test suite covering user journeys
Test Fixtures:
// fixtures/test-data.ts
export const testUsers = {
validUser: {
email: '[email protected]',
password: 'ValidPassword123!',
},
adminUser: {
email: '[email protected]',
password: 'AdminPassword123!',
},
newUser: {
email: '[email protected]',
password: 'NewPassword123!',
firstName: 'John',
lastName: 'Doe',
},
};
export const testProducts = {
product1: {
id: 'item-123',
name: 'Test Product',
price: 29.99,
},
};
Custom Fixtures:
// fixtures/custom-fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: Page;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: async ({ page }, use) => {
// Auto-login before test
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'ValidPassword123!');
await page.waitForURL('/dashboard');
await use(page);
},
});
export { expect } from '@playwright/test';
Using Custom Fixtures:
// tests/e2e/features/profile.spec.ts
import { test, expect } from '../../fixtures/custom-fixtures';
test.describe('User Profile', () => {
test('user can update profile information', async ({ authenticatedPage, dashboardPage }) => {
// Already logged in via authenticatedPage fixture
await dashboardPage.goToProfile();
// Test continues with authenticated context
await dashboardPage.updateProfile({
firstName: 'Jane',
lastName: 'Smith',
});
await expect(dashboardPage.profileName).toHaveText('Jane Smith');
});
});
Deliverable: Reusable test data and fixtures
Test Multiple Viewports:
// tests/e2e/responsive/layout.spec.ts
import { test, expect, devices } from '@playwright/test';
const viewports = [
{ name: 'Desktop', device: devices['Desktop Chrome'] },
{ name: 'Tablet', device: devices['iPad Pro'] },
{ name: 'Mobile', device: devices['iPhone 12'] },
];
viewports.forEach(({ name, device }) => {
test.describe(`${name} Layout`, () => {
test.use(device);
test('navigation menu displays correctly', async ({ page }) => {
await page.goto('/');
if (name === 'Mobile') {
// Mobile should show hamburger menu
await expect(page.locator('.hamburger-menu')).toBeVisible();
await expect(page.locator('.desktop-nav')).not.toBeVisible();
} else {
// Desktop/Tablet should show full navigation
await expect(page.locator('.desktop-nav')).toBeVisible();
await expect(page.locator('.hamburger-menu')).not.toBeVisible();
}
});
test('images are responsive', async ({ page }) => {
await page.goto('/products');
const images = page.locator('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const img = images.nth(i);
const bbox = await img.boundingBox();
if (bbox) {
// Images should not overflow viewport
expect(bbox.width).toBeLessThanOrEqual(device.viewport.width);
}
}
});
});
});
Deliverable: Tests covering responsive design
Screenshot Comparison:
// tests/e2e/visual/snapshot.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('homepage matches baseline', async ({ page }) => {
await page.goto('/');
// Take full page screenshot
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100,
});
});
test('modal dialog matches baseline', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="open-modal"]');
// Screenshot of specific element
const modal = page.locator('.modal');
await expect(modal).toHaveScreenshot('modal.png');
});
test('dark mode matches baseline', async ({ page }) => {
await page.goto('/');
// Enable dark mode
await page.evaluate(() => {
document.documentElement.setAttribute('data-theme', 'dark');
});
await expect(page).toHaveScreenshot('homepage-dark.png', {
fullPage: true,
});
});
});
Deliverable: Visual regression test suite
Mock API Responses:
// tests/e2e/network/api-mocking.spec.ts
import { test, expect } from '@playwright/test';
test.describe('API Mocking', () => {
test('handles slow API response gracefully', async ({ page }) => {
// Mock slow API
await page.route('**/api/products', async (route) => {
await new Promise(resolve => setTimeout(resolve, 3000));
await route.fulfill({
status: 200,
body: JSON.stringify({ products: [] }),
});
});
await page.goto('/products');
// Should show loading state
await expect(page.locator('.loading-spinner')).toBeVisible();
// Should eventually show products
await expect(page.locator('.product-list')).toBeVisible({ timeout: 5000 });
});
test('handles API error gracefully', async ({ page }) => {
// Mock API error
await page.route('**/api/products', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal server error' }),
});
});
await page.goto('/products');
// Should show error message
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toContainText('Failed to load products');
});
test('handles network timeout', async ({ page }) => {
await page.route('**/api/products', (route) => {
// Never fulfill, causing timeout
});
await page.goto('/products');
// Should show timeout message
await expect(page.locator('.timeout-message')).toBeVisible({ timeout: 10000 });
});
});
Deliverable: Tests for API interactions and error states
Reuse Authentication State:
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'ValidPassword123!');
await page.waitForURL('/dashboard');
// Save authentication state
await page.context().storageState({ path: authFile });
});
Use Saved Auth State:
// playwright.config.ts
export default defineConfig({
// ... other config
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Deliverable: Efficient authentication handling
Test Organization:
test.describe()Locator Strategy:
page.locator('[data-testid="submit"]')page.locator('button:has-text("Submit")')Waiting Strategy:
page.waitForTimeout())waitForLoadState() for page loadswaitFor() for specific elementsError Handling:
Performance:
test.describe.configure({ mode: 'parallel' })Maintainability:
Using MCP Tools:
// Example: Using Playwright MCP for navigation
test('navigate using MCP', async ({ page }) => {
// Use mcp__playwright-mcp__playwright_navigate
await page.evaluate(async () => {
// MCP navigation call would go here
});
await expect(page).toHaveURL('/expected-page');
});
// Example: Using MCP for screenshots
test('capture screenshot with MCP', async ({ page }) => {
await page.goto('/');
// Use mcp__playwright-mcp__playwright_screenshot
await page.evaluate(async () => {
// MCP screenshot call would go here
});
});
Your goal is to create robust, maintainable E2E tests that provide confidence in application functionality across browsers and devices.
Structured design dialogue that validates ideas before implementation begins.
Comprehensive design intelligence for web and mobile UI/UX across 10 technology stacks.
Comprehensive implementation plans for multi-step tasks, breaking down specs into bite-sized, testable steps.
Introduction to the obra skills system with mandatory skill invocation rules and best practices.
Execute a written implementation plan with critical review and task checkpoints.
Delegate independent tasks to specialized agents working concurrently with isolated context.
Isolated git worktrees with smart directory selection and safety verification.
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Plan searchable and shareable content that drives traffic, builds authority, and generates leads.
README-first repository scanner that extracts commands and classifies reproduction candidates without executing them.
Brainstorm and prioritize marketing strategies tailored to your SaaS stage, budget, and goals.
Plan and optimize your website's page hierarchy, navigation, URL structure, and internal linking.
The world's fastest calendar for remote work
Capture, organize, and utilize your knowledge effortlessly.
Instant Summaries of Audio & Video Interviews with AnySummary
Transform PDFs into engaging mind maps.
Chat with any PDF instantly
Create an optimal daily plan using your voice
A productivity platform to centralize organizational knowledge and workflows with contextual AI assistance.
AskYourPDF Pricing Plans: Tailored to Your Needs