🕒 Teaching Flow
1. Story Intro (5 min)
👉 "TypeScript is like adding a safety net to your JavaScript code. Instead of finding errors when your tests run (and fail), TypeScript catches them before you even run the tests. It's like having a spell-checker for your code that prevents typos and mistakes."
TypeScript Analogy:
Think of JavaScript as a loose, flexible language where you can put anything anywhere (like a messy desk). TypeScript is like organizing that desk with labeled drawers - you know exactly what goes where, and you can't accidentally put a pen in the paperclip drawer. This prevents mistakes before they happen.
Think of JavaScript as a loose, flexible language where you can put anything anywhere (like a messy desk). TypeScript is like organizing that desk with labeled drawers - you know exactly what goes where, and you can't accidentally put a pen in the paperclip drawer. This prevents mistakes before they happen.
Setup TypeScript (10 min)
Install TypeScript Dependencies
# Run in your Playwright project root npm install --save-dev typescript ts-node @types/node # Initialize TypeScript configuration npx tsc --init
📂 tsconfig.json
{
"compilerOptions": {
"target": "ESNext", // Use latest JavaScript features
"module": "CommonJS", // Module system for Node.js
"moduleResolution": "Node", // How to resolve modules
"strict": true, // Enable all strict type checking
"esModuleInterop": true, // Better compatibility with CommonJS
"outDir": "dist", // Where to put compiled files
"rootDir": ".", // Source files location
"skipLibCheck": true, // Skip type checking of declaration files
"forceConsistentCasingInFileNames": true
},
"include": [
"tests/**/*",
"pages/**/*",
"utils/**/*",
"test-data/**/*"
],
"exclude": [
"node_modules",
"dist",
"test-results",
"playwright-report"
]
}
Key TypeScript Configuration Benefits:
- strict: true - Catches more potential errors
- include - Specifies which files to compile
- exclude - Ignores unnecessary files
- outDir - Keeps compiled files separate
Update Playwright Configuration
📂 playwright.config.ts
import { defineConfig } 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: 'https://learn-playwright.great-site.net',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
JavaScript vs TypeScript Refresher (15 min)
Variables & Types
| JavaScript | TypeScript | Benefits |
|---|---|---|
// JS - No type information let message = "Hello"; let count = 10; let isLoggedIn = true; |
// TS - Explicit types let message: string = "Hello"; let count: number = 10; let isLoggedIn: boolean = true; |
Catches type errors at compile time |
Functions
| JavaScript | TypeScript | Benefits |
|---|---|---|
// JS - No parameter types
function add(x, y) {
return x + y;
}
function login(email, password) {
// Could pass wrong types!
}
|
// TS - Type-safe parameters
function add(x: number, y: number): number {
return x + y;
}
function login(email: string, password: string): void {
// TypeScript ensures correct types
}
|
Prevents runtime errors from wrong parameter types |
Objects & Interfaces
📂 TypeScript Interfaces
// Define structure for objects
interface Product {
name: string;
price: number;
inStock: boolean;
}
interface User {
email: string;
password: string;
firstName: string;
lastName: string;
}
// Use interfaces in functions
function addProductToCart(product: Product): void {
console.log(`Adding ${product.name} for $${product.price}`);
}
// Create objects that match interface
const dress: Product = {
name: "Printed Dress",
price: 29.99,
inStock: true
};
const testUser: User = {
email: "qa@example.com",
password: "password123",
firstName: "QA",
lastName: "Tester"
};
💡 Key Takeaway:
- JavaScript: Dynamic, errors found at runtime
- TypeScript: Static types, errors caught before running tests
- Result: More reliable, maintainable test code
Converting Existing E2E Flow to TypeScript (30 min)
Original JavaScript E2E Flow
📂 tests/E2E-userflow.spec.js (Original)
import { test, expect } from '@playwright/test';
import { homePage } from '../pages/homePage';
import { productPage } from '../pages/productPage';
import { cartPage } from '../pages/cartPage';
import { orderPage } from '../pages/orderPage';
import { myAccountPage } from '../pages/myAccountPage';
import { orderhistory } from '../pages/orderhistory';
import { DataManager } from '../utils/DataManager';
const data = JSON.parse(JSON.stringify(require("../test-data/test-data.json")));
test.describe("Validate e2e placing order", async () => {
test('complete order placement journey', async ({ page }) => {
const homepage = new homePage(page);
const productpage = new productPage(page);
const cartpage = new cartPage(page);
const orderpage = new orderPage(page);
const myaccountpage = new myAccountPage(page);
const orderhistoryPage = new orderhistory(page);
const datamanager = new DataManager();
await test.step("Verify search functionality on home page", async () => {
await homepage.goto();
await homepage.searchProduct(data.searchProduct);
await homepage.clickProduct(data.productName);
});
await test.step("Verify add to cart feature", async () => {
await productpage.checkProduct();
await productpage.clickaddtocartbutton();
await productpage.checkModal();
await productpage.clickProceedToCheckout();
});
await test.step("Verify proceed to checkout", async () => {
await cartpage.proceedCheckout();
});
await test.step("Verify address continue", async () => {
await orderpage.addressContinue();
});
await test.step("Verify shipping continue", async () => {
await orderpage.shippingContinue();
});
await test.step("Verify payment continue", async () => {
await orderpage.paymentContinue();
});
});
});
Converted TypeScript E2E Flow
📂 tests/E2E-userflow.spec.ts (TypeScript)
import { test, expect, Page } from '@playwright/test';
import { HomePage } from '../pages/HomePage';
import { ProductPage } from '../pages/ProductPage';
import { CartPage } from '../pages/CartPage';
import { OrderPage } from '../pages/OrderPage';
import { MyAccountPage } from '../pages/MyAccountPage';
import { OrderHistoryPage } from '../pages/OrderHistoryPage';
import { DataManager } from '../utils/DataManager';
import { TestData } from '../types/TestData';
// Import test data with proper typing
const testData: TestData = require("../test-data/test-data.json");
test.describe("Validate e2e placing order", () => {
test('complete order placement journey', async ({ page }: { page: Page }) => {
// Initialize page objects with proper typing
const homePage: HomePage = new HomePage(page);
const productPage: ProductPage = new ProductPage(page);
const cartPage: CartPage = new CartPage(page);
const orderPage: OrderPage = new OrderPage(page);
const myAccountPage: MyAccountPage = new MyAccountPage(page);
const orderHistoryPage: OrderHistoryPage = new OrderHistoryPage(page);
const dataManager: DataManager = new DataManager();
await test.step("Verify search functionality on home page", async (): Promise => {
await homePage.goto();
await homePage.searchProduct(testData.searchProduct);
await homePage.clickProduct(testData.productName);
});
await test.step("Verify add to cart feature", async (): Promise => {
await productPage.checkProduct();
await productPage.clickAddToCartButton();
await productPage.checkModal();
await productPage.clickProceedToCheckout();
});
await test.step("Verify proceed to checkout", async (): Promise => {
await cartPage.proceedCheckout();
});
await test.step("Verify address continue", async (): Promise => {
await orderPage.addressContinue();
});
await test.step("Verify shipping continue", async (): Promise => {
await orderPage.shippingContinue();
});
await test.step("Verify payment continue", async (): Promise => {
await orderPage.paymentContinue();
});
});
});
Converting Page Object Models to TypeScript (30 min)
HomePage.ts
📂 pages/HomePage.ts
import { Page, Locator, expect } from "@playwright/test";
export class HomePage {
// Explicitly type all locators
readonly page: Page;
readonly searchBox: Locator;
readonly searchButton: Locator;
readonly productLink: Locator;
readonly loginLink: Locator;
constructor(page: Page) {
this.page = page;
this.searchBox = page.getByPlaceholder("Search our catalog");
this.searchButton = page.getByRole("button", { name: "Search" });
this.productLink = page.getByRole("link", { name: "Printed Dress" });
this.loginLink = page.getByRole("link", { name: "Sign in" });
}
// All methods return Promise for async operations
async goto(): Promise {
await this.page.goto("https://learn-playwright.great-site.net/");
await this.page.waitForLoadState('networkidle');
}
async searchProduct(productName: string): Promise {
await this.searchBox.fill(productName);
await this.searchButton.click();
await this.page.waitForLoadState('networkidle');
}
async clickProduct(productName: string): Promise {
const productLocator = this.page.getByRole("link", { name: productName });
await expect(productLocator).toBeVisible();
await productLocator.click();
}
async clickLogin(): Promise {
await this.loginLink.click();
}
}
ProductPage.ts
📂 pages/ProductPage.ts
import { Page, Locator, expect } from "@playwright/test";
export class ProductPage {
readonly page: Page;
readonly productTitle: Locator;
readonly addToCartButton: Locator;
readonly proceedToCheckoutButton: Locator;
readonly modal: Locator;
readonly quantityInput: Locator;
constructor(page: Page) {
this.page = page;
this.productTitle = page.locator("h1");
this.addToCartButton = page.getByRole("button", { name: "Add to cart" });
this.proceedToCheckoutButton = page.getByRole("link", { name: "Proceed to checkout" });
this.modal = page.locator(".modal");
this.quantityInput = page.getByLabel("Quantity");
}
async checkProduct(): Promise {
await expect(this.productTitle).toBeVisible();
await expect(this.addToCartButton).toBeVisible();
}
async clickAddToCartButton(): Promise {
await this.addToCartButton.click();
await this.page.waitForLoadState('networkidle');
}
async checkModal(): Promise {
await expect(this.modal).toBeVisible();
await expect(this.proceedToCheckoutButton).toBeVisible();
}
async clickProceedToCheckout(): Promise {
await this.proceedToCheckoutButton.click();
await this.page.waitForLoadState('networkidle');
}
async setQuantity(quantity: number): Promise {
await this.quantityInput.fill(quantity.toString());
}
}
CartPage.ts
📂 pages/CartPage.ts
import { Page, Locator, expect } from "@playwright/test";
export class CartPage {
readonly page: Page;
readonly proceedCheckoutButton: Locator;
readonly cartItems: Locator;
readonly totalPrice: Locator;
readonly removeItemButton: Locator;
constructor(page: Page) {
this.page = page;
this.proceedCheckoutButton = page.getByRole("link", { name: "Proceed to checkout" });
this.cartItems = page.locator(".cart-item");
this.totalPrice = page.locator(".cart-total");
this.removeItemButton = page.getByRole("button", { name: "Remove" });
}
async proceedCheckout(): Promise {
await expect(this.proceedCheckoutButton).toBeVisible();
await this.proceedCheckoutButton.click();
await this.page.waitForLoadState('networkidle');
}
async verifyCartItems(expectedCount: number): Promise {
const itemCount = await this.cartItems.count();
expect(itemCount).toBe(expectedCount);
}
async getTotalPrice(): Promise {
const price = await this.totalPrice.textContent();
expect(price).not.toBeNull();
return price!;
}
async removeItem(): Promise {
await this.removeItemButton.click();
await this.page.waitForLoadState('networkidle');
}
}
OrderPage.ts
📂 pages/OrderPage.ts
import { Page, Locator, expect } from "@playwright/test";
export class OrderPage {
readonly page: Page;
readonly addressContinueButton: Locator;
readonly shippingContinueButton: Locator;
readonly paymentContinueButton: Locator;
readonly termsCheckbox: Locator;
readonly placeOrderButton: Locator;
readonly orderConfirmation: Locator;
constructor(page: Page) {
this.page = page;
this.addressContinueButton = page.getByRole("button", { name: "Continue" });
this.shippingContinueButton = page.getByRole("button", { name: "Continue" });
this.paymentContinueButton = page.getByRole("button", { name: "Continue" });
this.termsCheckbox = page.getByLabel("I agree to the terms of service");
this.placeOrderButton = page.getByRole("button", { name: "Place order" });
this.orderConfirmation = page.getByText("Order confirmation");
}
async addressContinue(): Promise {
await expect(this.addressContinueButton).toBeVisible();
await this.addressContinueButton.click();
await this.page.waitForLoadState('networkidle');
}
async shippingContinue(): Promise {
await expect(this.shippingContinueButton).toBeVisible();
await this.shippingContinueButton.click();
await this.page.waitForLoadState('networkidle');
}
async paymentContinue(): Promise {
await expect(this.paymentContinueButton).toBeVisible();
await this.paymentContinueButton.click();
await this.page.waitForLoadState('networkidle');
}
async placeOrder(): Promise {
await this.termsCheckbox.check();
await this.placeOrderButton.click();
await this.page.waitForLoadState('networkidle');
await expect(this.orderConfirmation).toBeVisible();
// Return order ID for further validation
const orderId = await this.page.locator(".order-reference").textContent();
expect(orderId).not.toBeNull();
return orderId!;
}
}
Creating Type Definitions (20 min)
Test Data Types
📂 types/TestData.ts
// Define interfaces for test data
export interface TestData {
searchProduct: string;
productName: string;
userCredentials: UserCredentials;
orderDetails: OrderDetails;
}
export interface UserCredentials {
email: string;
password: string;
firstName: string;
lastName: string;
}
export interface OrderDetails {
address: Address;
shipping: ShippingMethod;
payment: PaymentMethod;
}
export interface Address {
firstName: string;
lastName: string;
company: string;
address: string;
city: string;
state: string;
zipCode: string;
country: string;
phone: string;
}
export interface ShippingMethod {
method: string;
cost: number;
}
export interface PaymentMethod {
type: string;
cardNumber: string;
expiryDate: string;
cvv: string;
}
// Product interface for type safety
export interface Product {
name: string;
price: number;
description: string;
inStock: boolean;
category: string;
}
// Order interface for validation
export interface Order {
orderId: string;
productName: string;
quantity: number;
totalPrice: number;
orderDate: string;
status: string;
}
DataManager.ts
📂 utils/DataManager.ts
import fs from "fs";
import path from "path";
import { Order, Product } from "../types/TestData";
export class DataManager {
private readonly dataDir: string;
constructor() {
this.dataDir = path.join(process.cwd(), "test-data");
}
// Save order data with proper typing
async saveOrder(product: string, orderId: string): Promise {
const filePath = path.join(this.dataDir, `${product}-order.json`);
const orderData: Order = {
orderId,
productName: product,
quantity: 1,
totalPrice: 0, // Will be updated from actual order
orderDate: new Date().toISOString(),
status: "pending"
};
fs.writeFileSync(filePath, JSON.stringify(orderData, null, 2));
console.log(`✅ Order saved for ${product}: ${orderId}`);
}
// Get order data with type safety
async getOrder(product: string): Promise {
const filePath = path.join(this.dataDir, `${product}-order.json`);
if (!fs.existsSync(filePath)) {
throw new Error(`No order file found for product: ${product}`);
}
const orderData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
return orderData as Order;
}
// Load test data with proper typing
loadTestData(): TestData {
const filePath = path.join(this.dataDir, "test-data.json");
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
return data as TestData;
}
// Validate order data structure
validateOrder(order: Order): boolean {
return (
typeof order.orderId === "string" &&
typeof order.productName === "string" &&
typeof order.quantity === "number" &&
typeof order.totalPrice === "number" &&
typeof order.orderDate === "string" &&
typeof order.status === "string"
);
}
}
Advanced TypeScript Patterns (20 min)
Base Page Class
📂 pages/BasePage.ts
import { Page, Locator, expect } from "@playwright/test";
// Abstract base class for all page objects
export abstract class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
// Common methods available to all pages
async waitForPageLoad(): Promise {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string): Promise {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
async getPageTitle(): Promise {
const title = await this.page.title();
expect(title).not.toBeNull();
return title!;
}
async getCurrentUrl(): Promise {
return this.page.url();
}
// Abstract method that must be implemented by child classes
abstract verifyPageLoaded(): Promise;
}
Extended Page Classes
📂 pages/HomePage.ts (Extended)
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "./BasePage";
export class HomePage extends BasePage {
readonly searchBox: Locator;
readonly searchButton: Locator;
readonly productLink: Locator;
readonly loginLink: Locator;
constructor(page: Page) {
super(page); // Call parent constructor
this.searchBox = page.getByPlaceholder("Search our catalog");
this.searchButton = page.getByRole("button", { name: "Search" });
this.productLink = page.getByRole("link", { name: "Printed Dress" });
this.loginLink = page.getByRole("link", { name: "Sign in" });
}
// Implement abstract method from BasePage
async verifyPageLoaded(): Promise {
await expect(this.searchBox).toBeVisible();
await expect(this.loginLink).toBeVisible();
}
async goto(): Promise {
await this.page.goto("https://learn-playwright.great-site.net/");
await this.waitForPageLoad(); // Use inherited method
await this.verifyPageLoaded();
}
async searchProduct(productName: string): Promise {
await this.searchBox.fill(productName);
await this.searchButton.click();
await this.waitForPageLoad();
}
async clickProduct(productName: string): Promise {
const productLocator = this.page.getByRole("link", { name: productName });
await expect(productLocator).toBeVisible();
await productLocator.click();
}
}
Generic Utility Types
📂 types/Common.ts
// Generic types for reusability export type ApiResponse= { data: T; status: number; message: string; }; export type TestResult = { success: boolean; data?: T; error?: string; }; // Utility types for form data export type FormField = { name: string; value: string; required: boolean; }; export type ValidationResult = { isValid: boolean; errors: string[]; }; // Generic page locator type export type PageLocators = { [key: string]: any; // This will be Locator in actual usage }; // Test configuration type export type TestConfig = { baseUrl: string; timeout: number; retries: number; headless: boolean; browser: 'chromium' | 'firefox' | 'webkit'; };
🧑💻 Interactive Questions & Answers
1. Why does TS require explicit types like `Promise`?
Answer:
- Type Safety: TypeScript needs to know what a function returns to catch errors
- Promise
: Indicates the function is async and returns nothing (void) - IntelliSense: Better IDE support with autocomplete and error detection
- Documentation: Makes code self-documenting about what functions do
- Example:
// Without explicit typing - TypeScript guesses async function login() { // TypeScript doesn't know what this returns } // With explicit typing - Clear contract async function login(): Promise{ // TypeScript knows this returns nothing }
2. What error would JS allow but TS would block in our LoginPage?
Answer:
- Wrong Parameter Types: JS allows `login(123, true)` but TS blocks it
- Missing Parameters: JS allows `login("email")` but TS requires both email and password
- Undefined Variables: JS allows using undefined variables, TS catches them
- Example:
// JavaScript - This would run and cause runtime errors function login(email, password) { email.toUpperCase(); // What if email is null? password.length; // What if password is undefined? } // TypeScript - This catches errors at compile time function login(email: string, password: string): void { email.toUpperCase(); // TypeScript knows email is string password.length; // TypeScript knows password is string }
3. How do interfaces improve test data handling?
Answer:
- Structure Validation: Ensures test data has required fields
- Type Safety: Prevents using wrong data types in tests
- IntelliSense: IDE shows available properties and their types
- Refactoring Safety: Changes to interface update all usage
- Example:
interface User { email: string; password: string; firstName: string; } // TypeScript ensures all required fields are present const user: User = { email: "test@example.com", password: "password123", firstName: "John" // lastName missing - TypeScript error! };
4. How does TS make locators and POM more robust?
Answer:
- Locator Typing: `readonly searchBox: Locator` ensures proper Playwright types
- Method Signatures: Clear parameter and return types for all methods
- Constructor Safety: Ensures Page object is properly passed
- Property Access: TypeScript prevents accessing non-existent properties
- Example:
// TypeScript ensures proper Playwright types export class HomePage { readonly page: Page; // Must be Playwright Page readonly searchBox: Locator; // Must be Playwright Locator constructor(page: Page) { // Constructor must receive Page this.page = page; this.searchBox = page.getByPlaceholder("Search"); } async search(text: string): Promise{ // Clear method signature await this.searchBox.fill(text); } }
5. Can you still run .js tests in a TS project? (Yes, but better to migrate fully)
Answer:
- Yes, you can: TypeScript projects can run JavaScript files
- Mixed approach: Gradually migrate from JS to TS
- Better to migrate fully: Get full benefits of type safety
- Configuration: tsconfig.json can include both .js and .ts files
- Example:
// tsconfig.json - Can include both file types { "include": [ "tests/**/*.ts", // TypeScript files "tests/**/*.js", // JavaScript files (temporary) "pages/**/*.ts", "utils/**/*.ts" ] } // Gradual migration approach // Week 1: Convert page objects to TS // Week 2: Convert test files to TS // Week 3: Add interfaces and types // Week 4: Remove .js files
6. How do you handle optional properties in TypeScript interfaces?
Answer:
- Optional Properties: Use `?` to make properties optional
- Union Types: Use `|` for multiple possible types
- Default Values: Provide defaults for optional properties
- Example:
interface User { email: string; password: string; firstName?: string; // Optional property lastName?: string; // Optional property age?: number; // Optional number isActive: boolean; // Required property } // Usage with optional properties const user: User = { email: "test@example.com", password: "password123", isActive: true // firstName and lastName are optional };
7. How do you create reusable types for different test scenarios?
Answer:
- Generic Types: Create flexible types that work with different data
- Union Types: Combine multiple types for different scenarios
- Type Aliases: Create shortcuts for complex types
- Example:
// Generic type for different test data type TestData
= { scenario: string; data: T; expectedResult: string; }; // Union type for different user types type UserType = 'admin' | 'customer' | 'guest'; // Type alias for complex locator patterns type LocatorPattern = { selector: string; timeout?: number; visible?: boolean; }; // Usage const loginTest: TestData<{email: string, password: string}> = { scenario: "Valid login", data: { email: "test@example.com", password: "password123" }, expectedResult: "Dashboard loaded" };
8. How do you handle async/await patterns in TypeScript test methods?
Answer:
- Promise Return Types: Always specify Promise
for async methods - Error Handling: Use try-catch with proper typing
- Parallel Execution: Use Promise.all with typed arrays
- Example:
// Proper async method typing async function performLogin(credentials: UserCredentials): Promise
{ try { await page.fill('#email', credentials.email); await page.fill('#password', credentials.password); await page.click('#login-button'); return true; } catch (error) { console.error('Login failed:', error); return false; } } // Parallel async operations with typing async function loadMultiplePages(pages: Page[]): Promise { const promises: Promise [] = pages.map(page => page.goto('/')); await Promise.all(promises); }
🎯 Day 11 Homework Tasks
🟢 Beginner
Task 1
Convert DataManager.js into DataManager.ts with proper types for all methods and return values.
Task 2
Add explicit string type for all product names in page classes and create a Product interface.
🟡 Intermediate
Task 3
Create an interface OrderDetails and use it in CheckoutPage with proper typing for all order-related methods.
Task 4
Convert OrderHistoryPage.js into TS with typed rows: Locator and proper return types for all methods.
🔴 Advanced
Task 5
Create a generic BasePage.ts with readonly page: Page and extend it in all page classes with abstract methods.
Task 6
Create utility types for test data (User, Product, Order) and implement generic error handling patterns.
Best Practices & Tips
TypeScript Migration Best Practices:
- Start with page objects - they benefit most from typing
- Use interfaces for all test data structures
- Always specify return types for async methods
- Create a BasePage class for common functionality
- Use strict mode in tsconfig.json for maximum type safety
Common Pitfalls:
- Don't use
anytype - defeats the purpose of TypeScript - Avoid mixing .js and .ts files in the same project long-term
- Don't forget to update imports when renaming files
- Be careful with optional properties - handle undefined cases
- Don't ignore TypeScript errors - fix them immediately
Performance & Maintainability Tips:
- Use readonly for locators to prevent accidental modification
- Create type definitions in separate files for reusability
- Use generic types for common patterns
- Implement proper error handling with typed exceptions
- Use abstract classes for shared page object functionality
✅ Outcomes
- Understand setup of TypeScript in Playwright project
- Learn key JS vs TS differences (datatypes, functions, interfaces)
- Successfully convert E2E PrestaShop Order Flow into TypeScript
- Strengthen tests with type safety + interfaces
- Create reusable type definitions and utility types
- Implement advanced patterns like BasePage classes and generics
- Handle async/await patterns with proper TypeScript typing
- Apply best practices for maintainable TypeScript test automation