— No reviews yet
0 installs
16 views
0.0% view→install
Install
$ agentstack add skill-bobmatnyc-claude-mpm-skills-playwright ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Are you the author of Playwright? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claimAbout
Playwright E2E Testing Skill
progressivedisclosure: entrypoint: summary: "Modern E2E testing framework with cross-browser automation and built-in test runner" whentouse:
- "When testing web applications end-to-end"
- "When needing cross-browser testing"
- "When testing user flows and interactions"
- "When needing screenshot/video recording"
quick_start:
- "npm init playwright@latest"
- "Choose TypeScript and test location"
- "npx playwright test"
- "npx playwright show-report"
token_estimate: entry: 75-90 full: 4200-5200 ---
Overview
Playwright is a modern end-to-end testing framework that provides cross-browser automation with a built-in test runner, auto-wait mechanisms, and excellent developer experience.
Key Features
- Auto-wait: Automatically waits for elements to be ready
- Cross-browser: Chromium, Firefox, WebKit support
- Built-in runner: Parallel execution, retries, reporters
- Network control: Mock and intercept network requests
- Debugging: UI mode, trace viewer, inspector
Installation
# Initialize new Playwright project
npm init playwright@latest
# Or add to existing project
npm install -D @playwright/test
# Install browsers
npx playwright install
Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-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'] },
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Fundamentals
Basic Test Structure
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://example.com');
// Wait for element and check visibility
const title = page.locator('h1');
await expect(title).toBeVisible();
await expect(title).toHaveText('Example Domain');
// Get page title
await expect(page).toHaveTitle(/Example/);
});
test.describe('User authentication', () => {
test('should login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="username"]', 'testuser');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome-message')).toContainText('Welcome');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="username"]', 'invalid');
await page.fill('[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toHaveText('Invalid credentials');
});
});
Test Hooks
import { test, expect } from '@playwright/test';
test.describe('Dashboard tests', () => {
test.beforeEach(async ({ page }) => {
// Run before each test
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
});
test.afterEach(async ({ page }) => {
// Cleanup after each test
await page.close();
});
test.beforeAll(async ({ browser }) => {
// Run once before all tests in describe block
console.log('Starting test suite');
});
test.afterAll(async ({ browser }) => {
// Run once after all tests
console.log('Test suite complete');
});
test('displays user data', async ({ page }) => {
await expect(page.locator('.user-name')).toBeVisible();
});
});
Locator Strategies
Best Practice: Role-based Locators
import { test, expect } from '@playwright/test';
test('accessible locators', async ({ page }) => {
await page.goto('/form');
// By role (BEST - accessible and stable)
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('link', { name: 'Learn more' }).click();
// By label (good for forms)
await page.getByLabel('Password').fill('secret123');
// By placeholder
await page.getByPlaceholder('Search...').fill('query');
// By text
await page.getByText('Welcome back').click();
await page.getByText(/hello/i).isVisible();
// By test ID (good for dynamic content)
await page.getByTestId('user-profile').click();
// By title
await page.getByTitle('Close dialog').click();
// By alt text (images)
await page.getByAltText('User avatar').click();
});
CSS and XPath Locators
test('CSS and XPath locators', async ({ page }) => {
// CSS selectors
await page.locator('button.primary').click();
await page.locator('#user-menu').click();
await page.locator('[data-testid="submit-btn"]').click();
await page.locator('div.card:first-child').click();
// XPath (use sparingly)
await page.locator('xpath=//button[contains(text(), "Submit")]').click();
// Chaining locators
const form = page.locator('form#login-form');
await form.locator('input[name="email"]').fill('user@example.com');
await form.locator('button[type="submit"]').click();
// Filter locators
await page.getByRole('listitem')
.filter({ hasText: 'Product 1' })
.getByRole('button', { name: 'Add to cart' })
.click();
});
Page Object Model
Page Class Pattern
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByLabel('Username');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectErrorMessage(message: string) {
await this.errorMessage.waitFor({ state: 'visible' });
await expect(this.errorMessage).toHaveText(message);
}
}
// pages/DashboardPage.ts
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.locator('.welcome-message');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
}
async waitForLoad() {
await this.welcomeMessage.waitFor({ state: 'visible' });
}
async logout() {
await this.logoutButton.click();
}
}
// tests/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test('successful login flow', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboard = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await dashboard.waitForLoad();
await expect(dashboard.welcomeMessage).toContainText('Welcome');
});
Component Pattern
// components/NavigationComponent.ts
import { Page, Locator } from '@playwright/test';
export class NavigationComponent {
readonly page: Page;
readonly homeLink: Locator;
readonly profileLink: Locator;
readonly searchInput: Locator;
constructor(page: Page) {
this.page = page;
const nav = page.locator('nav');
this.homeLink = nav.getByRole('link', { name: 'Home' });
this.profileLink = nav.getByRole('link', { name: 'Profile' });
this.searchInput = nav.getByPlaceholder('Search...');
}
async navigateToProfile() {
await this.profileLink.click();
}
async search(query: string) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
}
}
User Interactions
Form Interactions
test('form interactions', async ({ page }) => {
await page.goto('/form');
// Text inputs
await page.fill('input[name="email"]', 'user@example.com');
await page.type('textarea[name="message"]', 'Hello', { delay: 100 });
// Checkboxes
await page.check('input[type="checkbox"][name="subscribe"]');
await page.uncheck('input[type="checkbox"][name="spam"]');
// Radio buttons
await page.check('input[type="radio"][value="option1"]');
// Select dropdowns
await page.selectOption('select[name="country"]', 'US');
await page.selectOption('select[name="color"]', { label: 'Blue' });
await page.selectOption('select[name="size"]', { value: 'large' });
// Multi-select
await page.selectOption('select[multiple]', ['value1', 'value2']);
// File uploads
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
await page.setInputFiles('input[type="file"]', [
'file1.jpg',
'file2.jpg'
]);
// Clear file input
await page.setInputFiles('input[type="file"]', []);
});
Mouse and Keyboard
test('mouse and keyboard interactions', async ({ page }) => {
// Click variations
await page.click('button');
await page.dblclick('button'); // Double click
await page.click('button', { button: 'right' }); // Right click
await page.click('button', { modifiers: ['Shift'] }); // Shift+click
// Hover
await page.hover('.tooltip-trigger');
await expect(page.locator('.tooltip')).toBeVisible();
// Drag and drop
await page.dragAndDrop('#draggable', '#droppable');
// Keyboard
await page.keyboard.press('Enter');
await page.keyboard.press('Control+A');
await page.keyboard.type('Hello World');
await page.keyboard.down('Shift');
await page.keyboard.press('ArrowDown');
await page.keyboard.up('Shift');
// Focus
await page.focus('input[name="email"]');
await page.fill('input[name="email"]', 'test@example.com');
});
Waiting Strategies
test('waiting strategies', async ({ page }) => {
// Wait for element
await page.waitForSelector('.dynamic-content');
await page.waitForSelector('.modal', { state: 'visible' });
await page.waitForSelector('.loading', { state: 'hidden' });
// Wait for load state
await page.waitForLoadState('load');
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle');
// Wait for URL
await page.waitForURL('**/dashboard');
await page.waitForURL(/\/product\/\d+/);
// Wait for function
await page.waitForFunction(() => {
return document.querySelectorAll('.item').length > 5;
});
// Wait for timeout (avoid if possible)
await page.waitForTimeout(1000);
// Wait for event
await page.waitForEvent('load');
await page.waitForEvent('popup');
});
Assertions
Common Assertions
import { test, expect } from '@playwright/test';
test('assertions', async ({ page }) => {
await page.goto('/dashboard');
// Visibility
await expect(page.locator('.header')).toBeVisible();
await expect(page.locator('.loading')).toBeHidden();
await expect(page.locator('.optional')).not.toBeVisible();
// Text content
await expect(page.locator('h1')).toHaveText('Dashboard');
await expect(page.locator('h1')).toContainText('Dash');
await expect(page.locator('.message')).toHaveText(/welcome/i);
// Attributes
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeDisabled();
await expect(page.locator('input')).toHaveAttribute('type', 'email');
await expect(page.locator('input')).toHaveValue('test@example.com');
// CSS
await expect(page.locator('.button')).toHaveClass('btn-primary');
await expect(page.locator('.button')).toHaveClass(/btn-/);
await expect(page.locator('.element')).toHaveCSS('color', 'rgb(255, 0, 0)');
// Count
await expect(page.locator('.item')).toHaveCount(5);
// URL and title
await expect(page).toHaveURL('http://localhost:3000/dashboard');
await expect(page).toHaveURL(/dashboard$/);
await expect(page).toHaveTitle('Dashboard - My App');
await expect(page).toHaveTitle(/Dashboard/);
// Screenshot comparison
await expect(page).toHaveScreenshot('dashboard.png');
await expect(page.locator('.widget')).toHaveScreenshot('widget.png');
});
Custom Assertions
test('custom matchers', async ({ page }) => {
// Soft assertions (continue test on failure)
await expect.soft(page.locator('.title')).toHaveText('Welcome');
await expect.soft(page.locator('.subtitle')).toBeVisible();
// Multiple elements
const items = page.locator('.item');
await expect(items).toHaveCount(3);
await expect(items.nth(0)).toContainText('First');
await expect(items.nth(1)).toContainText('Second');
// Poll assertions
await expect(async () => {
const response = await page.request.get('/api/status');
expect(response.ok()).toBeTruthy();
}).toPass({
timeout: 10000,
intervals: [1000, 2000, 5000],
});
});
Authentication Patterns
Storage State Pattern
// auth.setup.ts - Run once to save auth state
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="username"]', 'testuser');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
// Save authentication state
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: authFile,
},
dependencies: ['setup'],
},
],
});
// tests/dashboard.spec.ts - Already authenticated
test('view dashboard', async ({ page }) => {
await page.goto('/dashboard');
// Already logged in!
await expect(page.locator('.user-menu')).toBeVisible();
});
Multiple User Roles
// fixtures/auth.ts
import { test as base } from '@playwright/test';
type Fixtures = {
adminPage: Page;
userPage: Page;
};
export const test = base.extend({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
// tests/permissions.spec.ts
import { test } from '../fixtures/auth';
test('admin can access admin panel', async ({ adminPage }) => {
await adminPage.goto('/admin');
await expect(adminPage.locator('.admin-panel')).toBeVisible();
});
test('regular user cannot access admin p
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [bobmatnyc](https://github.com/bobmatnyc)
- **Source:** [bobmatnyc/claude-mpm-skills](https://github.com/bobmatnyc/claude-mpm-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.