Mastering Playwright TypeScript Script Examples: A Comprehensive Guide
Playwright with TypeScript represents a powerful combination for web automation, offering the type safety and developer experience of TypeScript alongside Playwright's robust browser automation capabilities. In this guide, we'll explore practical Playwright TypeScript script examples that will help you build reliable, maintainable, and scalable test automation for your web applications.
Introduction to Playwright with TypeScript
Playwright is an end-to-end testing framework developed by Microsoft that enables automation of modern web applications across all major browsers. When combined with TypeScript, it brings static typing, improved code completion, and better refactoring support to your test suites. The Playwright TypeScript script examples we'll explore demonstrate how to leverage TypeScript's strong typing to catch potential errors during development rather than at runtime, creating more reliable tests.
The benefits of using TypeScript with Playwright extend beyond just error prevention. With TypeScript, you can define interfaces for your page objects, create type-safe test data, and utilize advanced language features like enums and generics to make your tests more expressive and maintainable. As your test suite grows, these type safety features become increasingly valuable, helping you manage complexity and reduce the risk of introducing bugs during test maintenance.
TypeScript's static analysis capabilities complement Playwright's dynamic browser automation by providing compile-time checks that catch common errors before tests even run. This combination is particularly valuable for large test suites where maintaining consistency and reliability becomes challenging. The type safety ensures that method calls, parameter types, and return values are all validated during development, reducing the likelihood of runtime failures in your tests.
Setting Up Your Playwright TypeScript Environment
Before diving into Playwright TypeScript script examples, it's essential to set up your environment correctly. Begin by initializing a new Node.js project with npm or yarn. Then, install Playwright and its TypeScript dependencies:
npm init playwright@latest
npm install -D typescript @types/node
After installation, create a tsconfig.json file to configure TypeScript compilation options. This file is crucial for ensuring your TypeScript code is properly transformed into JavaScript that Playwright can execute:
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"lib": ["ES2017"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Your Playwright configuration can also be written in TypeScript (playwright.config.ts), allowing you to leverage TypeScript's type system for your configuration options:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
For optimal development experience, consider adding these scripts to your package.json:
{
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
"test:report": "playwright show-report",
"type-check": "tsc --noEmit",
"build": "tsc",
"clean": "rm -rf dist"
}
}
Writing Your First Playwright TypeScript Test
Let's create our first Playwright TypeScript script example. This test will navigate to a webpage, perform a simple interaction, and verify the result:
import { test, expect } from '@playwright/test';
test.describe('First Playwright TypeScript Example', () => {
test('should search and verify results', async ({ page }) => {
// Navigate to the search engine
await page.goto('https://www.google.com');
// Type in the search query
await page.fill('input[name="q"]', 'Playwright TypeScript');
// Click the search button
await page.click('input[type="submit"]');
// Wait for results to load and verify
await expect(page.locator('#search')).toBeVisible();
await expect(page.getByRole('link', { name: 'Playwright' })).toBeVisible();
});
});
This example demonstrates several TypeScript benefits:
- Type checking for Playwright's methods and parameters
- Better autocompletion and IDE support
- Clearer intent through method chaining
When running tests with TypeScript, you have two options:
- Use Playwright's built-in TypeScript transformation (works for most cases)
- Manually compile TypeScript to JavaScript before running tests (useful for experimental features)
To run your tests, simply execute:
npm test
For debugging, you can run tests in headed mode with:
npm run test:headed
Advanced Playwright TypeScript Script Examples
As your testing needs grow, you'll want to implement more sophisticated patterns. Let's explore a page object model example, which helps maintain test code by abstracting page interactions:
// pages/login.page.ts
import { Page, Locator } from 'playwright';
export class LoginPage {
private readonly usernameInput: Locator;
private readonly passwordInput: Locator;
private readonly loginButton: Locator;
private readonly errorMessage: Locator;
constructor(page: Page) {
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('#login-button');
this.errorMessage = page.locator('.error-message');
}
async login(username: string, password: string): Promise<void> {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorMessage(): Promise<string> {
return this.errorMessage.textContent();
}
async waitForPageLoad(): Promise<void> {
await this.usernameInput.waitFor({ state: 'visible' });
}
}
And here's how you would use this page object in a test:
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test.describe('Login Functionality', () => {
test('should display error for invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
// Navigate to login page
await page.goto('https://example.com/login');
await loginPage.waitForPageLoad();
// Attempt login with invalid credentials
await loginPage.login('invalid_user', 'wrong_password');
// Verify error message is displayed
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toContain('Invalid credentials');
});
});
For data-driven testing, you can use TypeScript interfaces to structure your test data:
interface LoginTestData {
username: string;
password: string;
expectedError: string;
}
const loginTestData: LoginTestData[] = [
{ username: 'user1', password: 'pass1', expectedError: 'Invalid password' },
{ username: 'user2', password: 'wrong', expectedError: 'Account locked' },
{ username: '', password: 'any', expectedError: 'Username required' }
];
test.describe('Data-Driven Login Tests', () => {
loginTestData.forEach((data) => {
test(`should handle login for user ${data.username}`, async ({ page }) => {
const loginPage = new LoginPage(page);
await page.goto('https://example.com/login');
await loginPage.waitForPageLoad();
await loginPage.login(data.username, data.password);
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toContain(data.expectedError);
});
});
});
Working with Browser Contexts and Pages
Playwright's browser context and page management system is one of its most powerful features. When working with TypeScript, you can create strongly-typed wrappers for these components to enhance your test structure:
// utils/browser-context.ts
import { Browser, BrowserContext, Page, chromium, firefox, webkit } from 'playwright';
export class BrowserManager {
private browser: Browser | null = null;
private context: BrowserContext | null = null;
async initialize(browserType: 'chromium' | 'firefox' | 'webkit' = 'chromium'): Promise<void> {
switch (browserType) {
case 'chromium':
this.browser = await chromium.launch();
break;
case 'firefox':
this.browser = await firefox.launch();
break;
case 'webkit':
this.browser = await webkit.launch();
break;
default:
throw new Error(`Unsupported browser type: ${browserType}`);
}
this.context = await this.browser.newContext({
viewport: { width: 1280, height: 720 },
acceptDownloads: true,
});
}
async newPage(): Promise<Page> {
if (!this.context) {
throw new Error('Browser context not initialized');
}
return await this.context.newPage();
}
async close(): Promise<void> {
if (this.context) {
await this.context.close();
this.context = null;
}
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
}
Here's how you can use this browser manager in your tests:
// tests/browser-manager.spec.ts
import { test, expect } from '@playwright/test';
import { BrowserManager } from '../utils/browser-context';
test.describe('Browser Context Management', () => {
let browserManager: BrowserManager;
test.beforeAll(async () => {
browserManager = new BrowserManager();
await browserManager.initialize('chromium');
});
test.afterAll(async () => {
await browserManager.close();
});
test('should create and manage multiple pages', async () => {
const page1 = await browserManager.newPage();
const page2 = await browserManager.newPage();
await page1.goto('https://example.com/page1');
await page2.goto('https://example.com/page2');
expect(await page1.title()).not.toBe(await page2.title());
await page1.close();
await page2.close();
});
});
Handling Network Requests and Responses
In modern web applications, network requests play a crucial role. Playwright provides excellent capabilities to intercept, modify, and monitor network requests. With TypeScript, you can create type-safe utilities for handling network operations:
// utils/network-interceptor.ts
import { Page, Request, Response } from 'playwright';
export interface NetworkHandler {
onRequest?: (request: Request) => Promise<void>;
onResponse?: (response: Response) => Promise<void>;
}
export class NetworkInterceptor {
private page: Page;
private handlers: NetworkHandler[] = [];
constructor(page: Page) {
this.page = page;
this.setupListeners();
}
addHandler(handler: NetworkHandler): void {
this.handlers.push(handler);
}
removeHandler(handler: NetworkHandler): void {
const index = this.handlers.indexOf(handler);
if (index !== -1) {
this.handlers.splice(index, 1);
}
}
private setupListeners(): void {
this.page.on('request', async (request) => {
for (const handler of this.handlers) {
if (handler.onRequest) {
await handler.onRequest(request);
}
}
});
this.page.on('response', async (response) => {
for (const handler of this.handlers) {
if (handler.onResponse) {
await handler.onResponse(response);
}
}
});
}
}
Here's an example of using the network interceptor to handle API calls:
// tests/network-interceptor.spec.ts
import { test, expect } from '@playwright/test';
import { NetworkInterceptor } from '../utils/network-interceptor';
test.describe('Network Request Handling', () => {
test('should intercept and modify API requests', async ({ page }) => {
const networkInterceptor = new NetworkInterceptor(page);
// Add a handler to modify API requests
networkInterceptor.addHandler({
onRequest: async (request) => {
if (request.url().includes('/api/data')) {
// Add authentication header
await request.setExtraHTTPHeaders({
'Authorization': 'Bearer test-token',
});
}
},
onResponse: async (response) => {
if (response.url().includes('/api/data')) {
// Log response status
console.log(`API response status: ${response.status()}`);
}
}
});
// Navigate to a page that makes API calls
await page.goto('https://example.com/dashboard');
// Verify that the API call was made with the modified headers
const apiResponse = await page.request.get('https://example.com/api/data');
expect(apiResponse.status()).toBe(200);
});
});
Organizing Your Test Suite
A well-organized test suite is crucial for maintainability as your automation grows. Consider these best practices for organizing your Playwright TypeScript project:
- Folder Structure:
tests/- Contains all test filespages/- Page object classesutils/- Helper functions and utilitiesdata/- Test data filesfixtures/- Test fixtures and setuptypes/- TypeScript type definitions
- Naming Conventions:
- Use descriptive test names that explain the scenario
- Group related tests in test.describe blocks
- Follow consistent naming for page objects
- Use kebab-case for test files and PascalCase for classes
For handling test setup and teardown, Playwright provides fixtures:
// fixtures/auth.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
// Login
await page.goto('https://example.com/login');
await loginPage.login('valid_user', 'valid_password');
// Use the authenticated page in tests
await use(page);
// Cleanup - logout
await page.click('#logout-button');
},
testData: {
user1: { username: 'testuser1', email: 'user1@example.com' },
user2: { username: 'testuser2', email: 'user2@example.com' }
}
});
export { expect };
You can then use these fixtures in your tests:
// tests/authenticated-tests.spec.ts
import { test, expect } from '../fixtures/auth';
test.describe('Authenticated User Tests', () => {
test('should access dashboard when authenticated', async ({ authenticatedPage }) => {
await authenticatedPage.goto('https://example.com/dashboard');
await expect(authenticatedPage.locator('#welcome-message')).toBeVisible();
});
test('should display user profile information', async ({ authenticatedPage, testData }) => {
await authenticatedPage.goto('https://example.com/profile');
// Verify user information is displayed correctly
await expect(authenticatedPage.locator('#username')).toHaveText(testData.user1.username);
await expect(authenticatedPage.locator('#email')).toHaveText(testData.user1.email);
});
});
Handling Assertions and Error Cases
Playwright provides a powerful assertion system that eliminates flaky timeouts and racy checks. When writing Playwright TypeScript script examples, it's important to understand how to use these assertions effectively:
Frequently Asked Questions
- What is Playwright TypeScript?
Playwright TypeScript combines Microsoft's Playwright browser automation framework with TypeScript's type safety, providing better error detection and code completion for test automation. - How do I set up Playwright with TypeScript?
Initialize a Node.js project, install Playwright and TypeScript dependencies, configure tsconfig.json, and set up playwright.config.ts for optimal development experience. - What are the benefits of using TypeScript with Playwright?
TypeScript provides static type checking, better IDE support, improved refactoring capabilities, and helps catch errors during development rather than at runtime. - How can I organize my Playwright TypeScript test suite?
Use a clear folder structure with separate directories for tests, page objects, utilities, and test data. Follow consistent naming conventions and leverage fixtures for setup and teardown. - What are some advanced patterns in Playwright TypeScript?
Implement page object models for better maintainability, use data-driven testing with TypeScript interfaces, create type-safe browser context managers, and handle network requests with typed utilities.
No comments:
Post a Comment