TypeScript-first testing guidelines with locator strategies, DRY principles, and Playwright-native patterns for robust end-to-end tests
A comprehensive set of guidelines for writing maintainable, reliable Playwright tests using TypeScript and following industry best practices for locator strategies and test architecture.
1. **TypeScript-First Development**
- Always use TypeScript instead of vanilla JavaScript
- Leverage type safety for better test reliability
- Use proper typing for locators, pages, and test fixtures
2. **Locator Strategy Hierarchy**
- Follow the priority order for element targeting to maximize test stability
- Prefer semantic locators over brittle CSS/XPath selectors
- Always save locators to variables before use
3. **DRY Principle with Helper Functions**
- Extract reusable logic into helper functions
- Keep helper functions focused on actions, not assertions
- Minimize function arguments to only what's necessary
4. **Playwright-Native Solutions**
- Use built-in Playwright features instead of custom implementations
- Leverage auto-waiting, retries, and assertions
- Consult official documentation before implementing custom logic
Follow this hierarchy when selecting element locators (from most to least preferred):
Always use test IDs when available. They provide the most stable and explicit element targeting.
```typescript
const signInButton = page.getByTestId("ExampleButton_signIn");
await signInButton.click();
```
Use role-based selectors for semantic HTML elements when test IDs aren't available.
```typescript
const submitButton = page.getByRole("button", { name: "Submit" });
await submitButton.click();
```
Use form-related semantic locators for inputs and images.
```typescript
const emailInput = page.getByPlaceholder("Enter your email");
const passwordField = page.getByLabel("Password");
const logo = page.getByAltText("Company Logo");
```
When using text-based locators, always use `{ exact: true }` to prevent partial matches.
```typescript
const heading = page.getByText("Welcome Back", { exact: true });
await expect(heading).toBeVisible();
```
Only use when semantic locators are unavailable. Keep selectors short and leverage available attributes.
```typescript
const customElement = page.locator('[data-component="custom-widget"]');
```
**DO:**
```typescript
const signInButton = page.getByTestId("ExampleButton_signIn");
await signInButton.click();
await expect(signInButton).toBeDisabled();
```
**DON'T:**
```typescript
await page.getByTestId("ExampleButton_signIn").click();
await expect(page.getByTestId("ExampleButton_signIn")).toBeDisabled();
```
Extract common workflows into helper functions, but keep them action-focused:
**DO:**
```typescript
async function loginUser(page: Page, email: string, password: string) {
const emailInput = page.getByPlaceholder("Email");
const passwordInput = page.getByPlaceholder("Password");
const submitButton = page.getByRole("button", { name: "Sign In" });
await emailInput.fill(email);
await passwordInput.fill(password);
await submitButton.click();
}
// In test file
await loginUser(page, "[email protected]", "password123");
await expect(page.getByText("Welcome", { exact: true })).toBeVisible();
```
**DON'T:**
```typescript
async function loginUser(page: Page, email: string, password: string, expectedUrl: string, welcomeMessage: string) {
const emailInput = page.getByPlaceholder("Email");
const passwordInput = page.getByPlaceholder("Password");
const submitButton = page.getByRole("button", { name: "Sign In" });
await emailInput.fill(email);
await passwordInput.fill(password);
await submitButton.click();
// Never assert in helper functions
await expect(page).toHaveURL(expectedUrl);
await expect(page.getByText(welcomeMessage)).toBeVisible();
}
```
**DO (Use `.toPass()` for retry logic):**
```typescript
const radioButton = page.getByTestId("option-1");
await expect(async () => {
await radioButton.click();
await expect(radioButton).toBeChecked();
}).toPass();
```
**DON'T (Custom retry implementation):**
```typescript
let attempts = 0;
let isChecked = await radioButton.isChecked();
while (!isChecked && attempts < 5) {
await radioButton.click();
isChecked = await radioButton.isChecked();
attempts++;
}
```
When implementing features, consult Playwright's official documentation:
https://playwright.dev/docs/intro
```typescript
import { test, expect, type Page } from '@playwright/test';
async function fillLoginForm(page: Page, email: string, password: string) {
const emailInput = page.getByTestId("Input_email");
const passwordInput = page.getByTestId("Input_password");
await emailInput.fill(email);
await passwordInput.fill(password);
}
test.describe('User Authentication', () => {
test('should login successfully with valid credentials', async ({ page }) => {
await page.goto('/login');
await fillLoginForm(page, '[email protected]', 'securePassword123');
const submitButton = page.getByRole('button', { name: 'Sign In' });
await submitButton.click();
const welcomeMessage = page.getByText('Welcome Back', { exact: true });
await expect(welcomeMessage).toBeVisible();
await expect(page).toHaveURL('/dashboard');
});
test('should show error with invalid credentials', async ({ page }) => {
await page.goto('/login');
await fillLoginForm(page, '[email protected]', 'wrongPassword');
const submitButton = page.getByRole('button', { name: 'Sign In' });
await submitButton.click();
const errorMessage = page.getByTestId("ErrorText_invalidCredentials");
await expect(errorMessage).toBeVisible();
});
});
```
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-pctymg/raw