Write Playwright E2E tests using fixtures and best practices. Use when creating E2E tests, writing browser automation tests, or testing user flows.
Writes Playwright E2E tests with accessibility-first queries, fixtures, and Page Object Model. Use when creating browser automation tests or testing user flows.
/plugin marketplace add gruckion/marathon-ralph/plugin install marathon-ralph@marathon-ralphThis skill is limited to using the following tools:
Write end-to-end tests using Playwright fixtures, Page Object Model, and accessibility-first queries.
Follow Kent C. Dodds' testing philosophy:
"The more your tests resemble the way your software is used, the more confidence they can give you."
Use queries in this order of preference:
// Buttons, links, headings
await page.getByRole('button', { name: /submit/i })
await page.getByRole('link', { name: /home/i })
await page.getByRole('heading', { level: 1 })
// Form elements
await page.getByRole('textbox', { name: /email/i })
await page.getByRole('checkbox', { name: /remember me/i })
await page.getByRole('combobox', { name: /country/i })
// With state
await page.getByRole('checkbox', { checked: true })
await page.getByRole('tab', { selected: true })
await page.getByLabel(/email address/i)
await page.getByLabel(/password/i)
await page.getByLabel(/phone number/i)
await page.getByText(/welcome back/i)
await page.getByText('Exact Match')
await page.getByPlaceholder('Search...')
// Only when no accessible alternative exists
await page.getByTestId('complex-widget')
// tests/e2e/fixtures/test-fixtures.ts
import { test as base } from '@playwright/test'
type MyFixtures = {
testUser: { email: string; password: string }
}
export const test = base.extend<MyFixtures>({
testUser: async ({}, use) => {
// Setup: Create test data
const user = {
email: `test-${Date.now()}@example.com`,
password: 'TestPassword123!',
}
// Provide to test
await use(user)
// Teardown: Cleanup (runs after test)
// await deleteUser(user.email)
},
})
export { expect } from '@playwright/test'
// tests/e2e/fixtures/auth-fixtures.ts
import { test as base, type Page } from '@playwright/test'
type AuthFixtures = {
authenticatedPage: Page
}
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
// Login before test
await page.goto('/login')
await page.getByLabel(/email/i).fill('user@example.com')
await page.getByLabel(/password/i).fill('password123')
await page.getByRole('button', { name: /sign in/i }).click()
await page.waitForURL('/dashboard')
// Provide authenticated page
await use(page)
// Logout after test (optional)
// await page.goto('/logout')
},
})
// tests/e2e/fixtures/db-fixtures.ts
import { test as base } from '@playwright/test'
type DbFixtures = {
insertTestData: (data: TestData) => Promise<string>
}
export const test = base.extend<DbFixtures>({
insertTestData: async ({ request }, use) => {
const createdIds: string[] = []
// Provide factory function
await use(async (data) => {
const response = await request.post('/api/test-data', { data })
const { id } = await response.json()
createdIds.push(id)
return id
})
// Cleanup all created data
for (const id of createdIds) {
await request.delete(`/api/test-data/${id}`)
}
},
})
// tests/e2e/fixtures/worker-fixtures.ts
import { test as base } from '@playwright/test'
type WorkerFixtures = {
sharedAccount: { username: string; token: string }
}
export const test = base.extend<{}, WorkerFixtures>({
sharedAccount: [
async ({ browser }, use, workerInfo) => {
// Create unique account per worker
const username = `worker-${workerInfo.workerIndex}`
// Setup runs once per worker
const page = await browser.newPage()
await page.goto('/signup')
// ... create account ...
await page.close()
await use({ username, token: 'token' })
// Teardown when worker shuts down
// await deleteAccount(username)
},
{ scope: 'worker' },
],
})
// tests/e2e/fixtures/index.ts
import { mergeTests } from '@playwright/test'
import { test as authTest } from './auth-fixtures'
import { test as dbTest } from './db-fixtures'
import { test as pageTest } from './page-fixtures'
export const test = mergeTests(authTest, dbTest, pageTest)
export { expect } from '@playwright/test'
// tests/e2e/pages/checkout.page.ts
import { type Page, type Locator, expect } from '@playwright/test'
export class CheckoutPage {
readonly page: Page
// Locators
readonly cartItems: Locator
readonly subtotal: Locator
readonly checkoutButton: Locator
readonly promoCodeInput: Locator
readonly applyPromoButton: Locator
readonly errorMessage: Locator
constructor(page: Page) {
this.page = page
this.cartItems = page.getByRole('list', { name: /cart items/i })
this.subtotal = page.getByTestId('subtotal')
this.checkoutButton = page.getByRole('button', { name: /checkout/i })
this.promoCodeInput = page.getByLabel(/promo code/i)
this.applyPromoButton = page.getByRole('button', { name: /apply/i })
this.errorMessage = page.getByRole('alert')
}
async goto() {
await this.page.goto('/checkout')
}
async applyPromoCode(code: string) {
await this.promoCodeInput.fill(code)
await this.applyPromoButton.click()
}
async proceedToCheckout() {
await this.checkoutButton.click()
}
async expectItemCount(count: number) {
await expect(this.cartItems.getByRole('listitem')).toHaveCount(count)
}
async expectSubtotal(amount: string) {
await expect(this.subtotal).toHaveText(amount)
}
}
// tests/e2e/fixtures/page-fixtures.ts
import { test as base } from '@playwright/test'
import { CheckoutPage } from '../pages/checkout.page'
import { LoginPage } from '../pages/login.page'
type PageFixtures = {
checkoutPage: CheckoutPage
loginPage: LoginPage
}
export const test = base.extend<PageFixtures>({
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page))
},
loginPage: async ({ page }, use) => {
await use(new LoginPage(page))
},
})
// tests/e2e/checkout.spec.ts
import { test, expect } from './fixtures'
test.describe('Checkout Flow', () => {
test('user can complete purchase', async ({ page, checkoutPage }) => {
// Given: User has items in cart
await checkoutPage.goto()
await checkoutPage.expectItemCount(2)
// When: User proceeds to checkout
await checkoutPage.proceedToCheckout()
// Then: User sees payment form
await expect(page).toHaveURL('/payment')
await expect(
page.getByRole('heading', { name: /payment/i })
).toBeVisible()
})
test('user can apply valid promo code', async ({ checkoutPage }) => {
await checkoutPage.goto()
await checkoutPage.applyPromoCode('SAVE20')
await expect(
checkoutPage.page.getByText(/20% discount applied/i)
).toBeVisible()
})
test('user sees error for invalid promo code', async ({ checkoutPage }) => {
await checkoutPage.goto()
await checkoutPage.applyPromoCode('INVALID')
await expect(checkoutPage.errorMessage).toHaveText(/invalid promo code/i)
})
})
import { test, expect } from './fixtures'
test.describe('Dashboard', () => {
test('authenticated user sees dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard')
await expect(
authenticatedPage.getByRole('heading', { name: /dashboard/i })
).toBeVisible()
await expect(
authenticatedPage.getByText(/welcome back/i)
).toBeVisible()
})
})
import { test, expect } from './fixtures'
test.describe('Products', () => {
test('user can view product details', async ({ page, insertTestData }) => {
// Create test product (auto-cleaned up after test)
const productId = await insertTestData({
name: 'Test Product',
price: 99.99,
})
await page.goto(`/products/${productId}`)
await expect(page.getByRole('heading')).toHaveText('Test Product')
await expect(page.getByText('$99.99')).toBeVisible()
})
})
Always use web-first assertions that auto-wait:
// GOOD - Auto-waits and retries
await expect(page.getByText('Success')).toBeVisible()
await expect(page.getByRole('button')).toBeEnabled()
await expect(page).toHaveURL('/dashboard')
await expect(page).toHaveTitle(/Dashboard/)
// BAD - Manual check, no retry
const isVisible = await page.getByText('Success').isVisible()
expect(isVisible).toBe(true)
// Visibility
await expect(locator).toBeVisible()
await expect(locator).toBeHidden()
// Text content
await expect(locator).toHaveText('exact text')
await expect(locator).toContainText('partial')
// Attributes
await expect(locator).toHaveAttribute('href', '/path')
await expect(locator).toHaveClass(/active/)
// Form state
await expect(locator).toBeEnabled()
await expect(locator).toBeDisabled()
await expect(locator).toBeChecked()
await expect(locator).toHaveValue('input value')
// Count
await expect(locator).toHaveCount(5)
// Page
await expect(page).toHaveURL('/path')
await expect(page).toHaveTitle('Page Title')
Continue test even if assertion fails:
test('multiple checks', async ({ page }) => {
await page.goto('/dashboard')
// Soft assertions don't stop the test
await expect.soft(page.getByTestId('status')).toHaveText('Active')
await expect.soft(page.getByTestId('count')).toHaveText('10')
// Test continues even if above fail
await page.getByRole('link', { name: /settings/i }).click()
})
// Wait for specific API response
await page.goto('/dashboard')
await page.waitForResponse((response) =>
response.url().includes('/api/data') && response.status() === 200
)
const fileInput = page.getByLabel(/upload file/i)
await fileInput.setInputFiles('./test-data/file.pdf')
page.on('dialog', (dialog) => dialog.accept())
await page.getByRole('button', { name: /delete/i }).click()
await page.goto('/dashboard')
await page.screenshot({ path: 'dashboard.png' })
page.locator('.class-name') - prefer role/label queriespage.waitForTimeout(1000) - use web-first assertionstests/e2e/
├── fixtures/
│ ├── index.ts # Combined fixtures export
│ ├── auth-fixtures.ts # Authentication
│ ├── db-fixtures.ts # Database operations
│ └── page-fixtures.ts # Page objects
├── pages/
│ ├── login.page.ts
│ ├── dashboard.page.ts
│ └── checkout.page.ts
├── auth.setup.ts # Auth setup project
├── auth.spec.ts # Auth tests (one login flow)
├── dashboard.spec.ts
├── checkout.spec.ts
└── search.spec.ts
user can complete checkout not test1This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.