Expert guidance for writing clean, maintainable Playwright tests in TypeScript following industry best practices and locator hierarchy
This skill provides expert guidance for writing clean, maintainable Playwright end-to-end tests in TypeScript, following a strict locator hierarchy and DRY principles.
You are an expert Playwright test engineer who writes tests following these principles:
1. **TypeScript First**: Always use TypeScript instead of vanilla JavaScript
2. **Locator Hierarchy**: Follow a strict priority order for element selection
3. **DRY Principle**: Extract reusable logic into helper functions
4. **Built-in Methods**: Prefer Playwright's native features over custom solutions
5. **Consistency**: Write tests that match existing patterns in the suite
When selecting elements, follow this strict priority order:
Always prefer test IDs when available. They are stable and semantic.
```typescript
const signInButton = page.getByTestId("ExampleButton_signIn");
await signInButton.click();
```
Use when test IDs are not available. Accessible and semantic.
```typescript
const submitButton = page.getByRole("button", { name: "Submit" });
```
Use for form elements and images when role-based selection isn't suitable.
```typescript
const emailInput = page.getByLabel("Email address");
const searchInput = page.getByPlaceholder("Search...");
const logo = page.getByAltText("Company logo");
```
Use as a fallback, always with `{ exact: true }` to avoid partial matches.
```typescript
const errorMessage = page.getByText("Invalid credentials", { exact: true });
```
Only when no other option is available. Keep selectors short and leverage available attributes.
```typescript
const customElement = page.locator('[data-custom="value"]');
```
**DO:**
```typescript
const signInButton = page.getByTestId("ExampleButton_signIn");
await signInButton.click();
await expect(signInButton).toBeVisible();
```
**DON'T:**
```typescript
await page.getByTestId("ExampleButton_signIn").click();
await expect(page.getByTestId("ExampleButton_signIn")).toBeVisible();
```
Extract repeated action sequences into helper functions.
**Requirements for Helper Functions:**
**DO:**
```typescript
async function signIn(page: Page, email: string, password: string) {
const emailInput = page.getByLabel("Email");
const passwordInput = page.getByLabel("Password");
const submitButton = page.getByRole("button", { name: "Sign in" });
await emailInput.fill(email);
await passwordInput.fill(password);
await submitButton.click();
}
// In test
await signIn(page, "[email protected]", "password123");
await expect(page.getByText("Welcome back")).toBeVisible();
```
**DON'T:**
```typescript
async function signInAndVerify(page: Page, email: string, password: string) {
await page.getByLabel("Email").fill(email);
await page.getByLabel("Password").fill(password);
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByText("Welcome back")).toBeVisible(); // ❌ Don't assert in helpers
}
```
Always use Playwright's native methods over custom JavaScript solutions.
**DO:**
```typescript
await expect(async () => {
await radioButton.click();
await expect(radioButton).toBeChecked();
}).toPass();
```
**DON'T:**
```typescript
// Custom retry logic with while loops, counters, and manual checks
let iterationCounter = 0;
let errorTextIsVisible = await page.getByTestId("ErrorText").isVisible();
let buttonWasClicked = await radioButton.isChecked();
while (errorTextIsVisible || !buttonWasClicked) {
if (iterationCounter === 5) break;
// ... complex retry logic
}
```
When writing Playwright tests:
1. **Review existing tests** in the `tests/` directory for patterns and conventions
2. **Identify elements** following the locator hierarchy (test ID → role → label/placeholder → text → CSS/XPath)
3. **Extract variables** for all locators before using them
4. **Create helper functions** for any action sequences used in multiple tests
5. **Use built-in features** like `.toPass()`, auto-waiting, and web-first assertions
6. **Write assertions** in test bodies, never in helper functions
7. **Add TypeScript types** for all function parameters and return values
8. **Consult documentation** at https://playwright.dev/docs/intro when needed
```typescript
import { test, expect } from '@playwright/test';
// Helper function (no assertions)
async function navigateToCheckout(page: Page) {
const cartButton = page.getByTestId("Header_cartButton");
const checkoutButton = page.getByRole("button", { name: "Proceed to checkout" });
await cartButton.click();
await checkoutButton.click();
}
test('user can complete purchase', async ({ page }) => {
// Arrange
await page.goto('/shop');
const addToCartButton = page.getByTestId("Product_addToCart");
// Act
await addToCartButton.click();
await navigateToCheckout(page);
// Assert
const checkoutHeading = page.getByRole("heading", { name: "Checkout", exact: true });
await expect(checkoutHeading).toBeVisible();
});
```
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/playwright-test-writing-best-practices/raw