Answer
Writing Playwright Tests in TypeScript
TypeScript is the recommended and default language for Playwright. It provides full type safety, autocomplete, and catches errors at development time.
Project Setup
npm init playwright@latest
# Choose "TypeScript" (default)
Playwright runs TypeScript tests directly without a separate compilation step — it uses its own transpiler internally.
Basic TypeScript Test
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('successful login', async ({ page }) => {
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('pass123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
});
TypeScript Page Object Model
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
private readonly emailInput: Locator;
private readonly passwordInput: Locator;
private readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.locator('[data-testid="error-message"]');
}
async navigate(): Promise<void> {
await this.page.goto('/login');
}
async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('login with typed POM', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('user@test.com', 'pass123');
await expect(page).toHaveURL('/dashboard');
});
TypeScript Custom Fixtures
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
type Fixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';
// tests/dashboard.spec.ts
import { test, expect } from '../fixtures';
test('dashboard loads correctly', async ({ loginPage, dashboardPage }) => {
await loginPage.navigate();
await loginPage.login('admin@test.com', 'admin123');
await expect(dashboardPage.title).toBeVisible();
});
TypeScript tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist"
},
"include": ["tests/**/*", "pages/**/*", "fixtures.ts"]
}
Key Advantage: TypeScript catches type mismatches in Page Objects at compile time — you get autocomplete for all Playwright APIs and your own page object methods.
