TypeScript-based Playwright test automation with locator hierarchy, DRY principles, and built-in utility usage
A comprehensive guide for writing maintainable, robust Playwright tests in TypeScript following industry best practices for locator selection, code reusability, and leveraging Playwright's built-in features.
Always use TypeScript instead of vanilla JavaScript for all Playwright test code.
Follow this strict priority order when selecting elements:
1. **First choice: `getByTestId()`**
- Use test ID attributes whenever possible for stable, semantic element targeting
- Example: `page.getByTestId("ExampleButton_signIn")`
2. **Second choice: `getByRole()`**
- Use when test IDs are unavailable
- Leverages semantic HTML roles for accessibility-aligned testing
- Example: `page.getByRole("button", { name: "Sign In" })`
3. **Third choice: `getByPlaceholder()`, `getByLabel()`, or `getByAltText()`**
- Use when role-based locators are insufficient
- These methods target form inputs and labeled elements
- Example: `page.getByLabel("Email Address")`
4. **Fourth choice: `getByText()`**
- Use as a fallback for visible text content
- **Always use `{ exact: true }` option** to prevent partial matches
- Example: `page.getByText("Welcome", { exact: true })`
5. **Last resort: XPath or CSS selectors**
- Only when none of the above methods work
- Keep selectors as short as possible
- Leverage available attributes to create stable selectors
Always save locators to variables before using them. Never chain locator calls directly in actions.
**Do:**
```typescript
const exampleSignInButton = page.getByTestId("ExampleButton_signIn");
await exampleSignInButton.click();
```
**Don't:**
```typescript
await page.getByTestId("ExampleButton_signIn").click();
```
When logic is reused across multiple tests, create helper/utility functions to avoid duplication.
**Helper function guidelines:**
**Example:**
```typescript
// Good helper function - performs actions only
async function signInUser(page: Page, email: string, password: string) {
const emailInput = page.getByLabel("Email");
const passwordInput = page.getByLabel("Password");
const signInButton = page.getByTestId("Button_signIn");
await emailInput.fill(email);
await passwordInput.fill(password);
await signInButton.click();
}
// Assertions stay in the test
test("user can sign in", async ({ page }) => {
await signInUser(page, "[email protected]", "password123");
await expect(page.getByTestId("UserDashboard")).toBeVisible();
});
```
Always prefer Playwright's native methods over custom JavaScript/TypeScript implementations.
**Do - Use Playwright's retry mechanism:**
```typescript
await expect(async () => {
await radioButton.click();
await expect(radioButton).toBeChecked();
}).toPass();
```
**Don't - Implement custom retry logic:**
```typescript
export async function radioButtonGroupClick(
page: Page,
urlToWaitFor: string,
radioButton: Locator,
errorTextTrigger: Locator
) {
await page.waitForURL(urlToWaitFor);
let iterationCounter = 0;
let errorTextIsVisible = await page.getByTestId("ErrorText").isVisible();
let buttonWasClicked = await radioButton.isChecked();
while (errorTextIsVisible || !buttonWasClicked) {
if (iterationCounter === 5) {
break;
} else {
iterationCounter++;
}
await radioButton.click();
buttonWasClicked = await radioButton.isChecked();
if (buttonWasClicked) {
await errorTextTrigger.click();
errorTextIsVisible = await page.getByTestId("ErrorText").isVisible();
}
}
}
```
Playwright's built-in features include:
Consult the official documentation: https://playwright.dev/docs/intro
When writing new tests, follow the patterns established in the existing `tests/` directory. Maintain consistency in:
```typescript
import { test, expect, type Page } from "@playwright/test";
test.describe("Authentication Flow", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/login");
});
test("should sign in with valid credentials", async ({ page }) => {
const emailInput = page.getByLabel("Email");
const passwordInput = page.getByLabel("Password");
const signInButton = page.getByTestId("Button_signIn");
const successMessage = page.getByText("Welcome back!", { exact: true });
await emailInput.fill("[email protected]");
await passwordInput.fill("SecurePassword123");
await signInButton.click();
await expect(successMessage).toBeVisible();
});
test("should show error with invalid credentials", async ({ page }) => {
const emailInput = page.getByLabel("Email");
const passwordInput = page.getByLabel("Password");
const signInButton = page.getByTestId("Button_signIn");
const errorMessage = page.getByTestId("ErrorText");
await emailInput.fill("[email protected]");
await passwordInput.fill("WrongPassword");
await signInButton.click();
await expect(errorMessage).toBeVisible();
await expect(errorMessage).toContainText("Invalid credentials");
});
});
```
1. TypeScript everywhere
2. Stable locator hierarchy (test IDs → roles → labels → text → CSS/XPath)
3. Save locators to variables
4. Extract reusable actions into helper functions (no assertions)
5. Use Playwright's built-in utilities instead of reinventing solutions
6. Maintain consistency with existing test patterns
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/playwright-testing-best-practices/raw