Migrating Playwright E2E Tests to TypeScript

Master TypeScript migration for robust, type-safe Playwright test automation

🕒 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.

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 any type - 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