Playwright vs Selenium vs Cypress: Which Should You Choose in 2026?

Playwright, Selenium and Cypress are the three most popular web test automation tools, and choosing between them is one of the first decisions every QA team makes. This guide compares them side by side, shows the same test written in each, and gives a clear recommendation for different situations.

πŸ“– What You’ll Learn: what each tool is, a full feature comparison table, architecture differences, the same login test in all three, which is easier to learn, job trends, a Selenium to Playwright concept map, and pros and cons.


⚑ Quick Answer

  • Choose Playwright for new web automation projects: fast setup, automatic waiting, built-in parallel runs, API testing and excellent debugging tools, all free.
  • Choose Selenium when you need a language Playwright does not support (such as Ruby or PHP), real Safari on macOS through WebDriver, legacy browsers, or you already have a large Selenium suite and Grid.
  • Choose Cypress for a JavaScript-only team that mainly tests a single front-end app and wants an interactive, in-browser runner, and that does not need multiple tabs or several browser windows in one test.

🧭 What Each Tool Is

Playwright

An open-source end-to-end testing framework released by Microsoft in 2020, built by engineers who previously created Puppeteer. One API controls Chromium, Firefox and WebKit. It ships with its own test runner (@playwright/test), assertions, reporters, codegen, UI mode and Trace Viewer.

Selenium

The original browser automation project, started in 2004. Selenium WebDriver became the W3C WebDriver standard that browser vendors implement. Selenium is a library, not a full framework: you combine it with a test runner such as TestNG, JUnit or pytest, and with Selenium Grid for distributed runs.

Cypress

A JavaScript testing tool launched around 2017 that runs inside the browser, next to your application. It is known for its interactive runner with time-travel snapshots and is popular with front-end developers.


πŸ“Š Feature Comparison Table

FeaturePlaywrightSeleniumCypress
First released202020042017
LanguagesJavaScript, TypeScript, Python, Java, .NETJava, Python, C#, JavaScript, Ruby, Kotlin and moreJavaScript, TypeScript
BrowsersChromium (Chrome, Edge), Firefox, WebKitChrome, Edge, Firefox, Safari, IE modeChrome-family, Firefox, Electron; WebKit experimental
ArchitectureTalks to browsers over a WebSocket connectionW3C WebDriver protocol through browser driversRuns inside the browser with the app
Test runnerBuilt inBring your own (TestNG, JUnit, pytest)Built in
WaitingAutomatic actionability checksExplicit and implicit waitsAutomatic retries
Multiple tabs and windowsYesYesNo
Cross-origin navigationYesYesLimited, needs cy.origin
iFramesSimple with frameLocatorswitchTo().frameWorkarounds needed
Parallel executionBuilt in and free (workers, sharding)Selenium Grid or runner pluginsCypress Cloud (paid) or third-party tools
API testingBuilt in (request fixture)Needs another librarycy.request
Network mockingpage.routeLimited (BiDi / CDP)cy.intercept
DebuggingTrace Viewer, UI mode, Inspector, codegenLogs, screenshots, IDE debuggerTime-travel runner, DevTools
MobileBrowser emulation of devicesReal devices via AppiumViewport resizing
CostFree, Apache 2.0Free, Apache 2.0Free runner, paid cloud

πŸ—οΈ Architecture Differences (Why Playwright Feels Faster)

Selenium sends each command (click, type, find) as an HTTP request to a browser driver such as ChromeDriver, which then controls the browser. Every step is a separate round trip, and your code must wait explicitly for elements to be ready.

Playwright keeps one persistent WebSocket connection open to the browser and sends commands over it. Before every action it automatically checks that the element is attached, visible, stable, enabled and able to receive events. Each test also gets a fresh browser context, which is like a brand-new incognito profile that starts in milliseconds, so tests are isolated without restarting the browser.

Cypress injects your test code into the browser itself. This gives it direct access to the app and a great live view, but it is also why it cannot drive two tabs or two browsers at once.

πŸ“ Note: Selenium 4 added WebDriver BiDi and Chrome DevTools support, which narrows some gaps (such as network events). Playwright’s advantages in setup, waiting and tooling are still significant for most teams.


πŸ§‘β€πŸ’» The Same Test in All Three Tools

A login test on the Sauce Demo site, to compare how much code and waiting each needs.

Playwright (TypeScript)

import { test, expect } from '@playwright/test';

test('login', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page.locator('.title')).toHaveText('Products');
});

Selenium (Python)

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://www.saucedemo.com/")
driver.find_element(By.ID, "user-name").send_keys("standard_user")
driver.find_element(By.ID, "password").send_keys("secret_sauce")
driver.find_element(By.ID, "login-button").click()

title = WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.CLASS_NAME, "title"))
)
assert title.text == "Products"
driver.quit()

Cypress (JavaScript)

describe('login', () => {
  it('logs in', () => {
    cy.visit('https://www.saucedemo.com/');
    cy.get('[data-test="username"]').type('standard_user');
    cy.get('[data-test="password"]').type('secret_sauce');
    cy.get('[data-test="login-button"]').click();
    cy.get('.title').should('have.text', 'Products');
  });
});

Notice that Playwright and Cypress need no explicit wait, while Selenium uses WebDriverWait for the final check. Playwright also needs no driver management or separate runner configuration.


πŸ“ˆ Which Is Easier to Learn?

For beginners, Playwright and Cypress are easier to start with than Selenium: one install command, a built-in runner and automatic waiting remove the most common causes of flaky tests. Between the two, Playwright is more flexible once you go beyond basic UI flows (multiple tabs, iframes, API tests, several browsers), while Cypress has the gentlest first hour thanks to its visual runner.

Selenium has a steeper start because you assemble the framework yourself (driver, runner, waits, reporting), but it teaches concepts that apply everywhere, and its language support is the widest.


πŸ’Ό Jobs and Industry Trend

Selenium still appears in a very large number of job descriptions because of the huge number of existing suites. Playwright has grown the fastest of the three in recent years (in npm downloads, GitHub stars and job mentions), and many teams now choose it for new projects or migrate to it. Learning Playwright with TypeScript, while understanding Selenium concepts, is a strong combination for QA automation roles in 2026.


πŸ”„ Moving from Selenium to Playwright: Concept Map

SeleniumPlaywright
driver.get(url)page.goto(url)
driver.findElement(By.id(‘x’))page.locator(‘#x’) or page.getByRole(…)
sendKeys(‘text’)fill(‘text’)
WebDriverWait + ExpectedConditionsAuto-waiting and expect(locator).toBeVisible()
new Select(el).selectByVisibleText(‘A’)locator.selectOption({ label: ‘A’ })
driver.switchTo().frame(‘f’)page.frameLocator(‘#f’)
driver.switchTo().alert().accept()page.on(‘dialog’, d => d.accept())
getWindowHandles() and switchTo().window()page.waitForEvent(‘popup’)
Actions(driver).moveToElement(el)locator.hover()
Selenium GridBuilt-in workers and –shard

Want practical examples? See Playwright waits explained and handling alerts, popups, iframes and tabs in Playwright. If you are coming from Python Selenium, compare with our Selenium waits and Selenium locators guides.


βš–οΈ Pros and Cons Summary

Playwright

  • βœ… Fast, reliable auto-waiting; free parallel runs; API testing and network mocking built in
  • βœ… Best-in-class debugging with Trace Viewer, UI mode and codegen
  • ❌ Uses its own builds of Firefox and WebKit rather than the exact branded Safari; no native mobile app testing

Selenium

  • βœ… Widest language and browser support, W3C standard, huge community
  • βœ… Real mobile testing through Appium and mature Grid infrastructure
  • ❌ More boilerplate, manual waits, and you must assemble the framework yourself

Cypress

  • βœ… Very friendly interactive runner and great developer experience
  • βœ… Strong for front-end and component testing
  • ❌ JavaScript only, no multi-tab support, parallel runs mainly through a paid cloud

❓ FAQ

πŸ€” Is Playwright better than Selenium?

For most new web projects, yes: it is faster to set up, less flaky and includes more built-in features. Selenium remains better when you need its languages, real branded browsers through WebDriver, Appium mobile testing or an existing Grid.

πŸ€” Is Playwright faster than Selenium?

Usually yes. A persistent connection, lightweight browser contexts and automatic waiting reduce overhead and retries. Actual speed depends on your application and test design.

πŸ€” Should I learn Selenium or Playwright in 2026?

If you are starting fresh, learn Playwright with TypeScript first, then learn Selenium concepts so you can work on existing suites. Both skills appear in job descriptions.

πŸ€” Can Playwright replace Cypress?

Yes for end-to-end testing. Playwright covers everything Cypress does for E2E plus multiple tabs, more browsers and free parallelism. Some teams keep Cypress specifically for component testing.

πŸ€” Does Playwright support Safari?

Playwright tests WebKit, the engine behind Safari, on Windows, macOS and Linux. It does not drive the branded Safari app itself; Selenium with safaridriver does.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

TypeScript for Playwright: Complete Beginner Tutorial (Setup, Concepts and Framework)

This is the bridge between learning TypeScript and building real Playwright automation. You will set up a Playwright TypeScript project, learn exactly which TypeScript concepts matter for testing, and build typed tests, page objects and fixtures step by step.

πŸ“– What You’ll Learn: why TypeScript for Playwright, the concepts you need, project setup, playwright.config.ts, async/await and Promise, Page and Locator types, typed test data, page object classes, generic fixtures, type checking with tsc, and common errors.


🎯 Why Use TypeScript with Playwright?

Playwright supports JavaScript, TypeScript, Python, Java and .NET, but TypeScript is the most popular choice for Playwright projects, and it is what the official documentation uses in its examples. The reasons are practical:

  • Autocomplete everywhere: type page. and your editor lists every method with its parameters.
  • Errors before you run: a typo like page.gotoo() or a missing await on a typed value is flagged instantly.
  • Zero setup: Playwright runs .ts test files directly; you do not need to configure a separate compiler.
  • Safer refactoring: rename a page object method and every usage is updated or highlighted.
  • Better job prospects: “Playwright + TypeScript” is one of the most requested combinations in QA automation job posts.

πŸ—ΊοΈ TypeScript Concepts You Need for Playwright

You do not need to master all of TypeScript before writing tests. This is the practical checklist, in learning order, with a guide for each:

ConceptWhere you use it in PlaywrightGuide
Variables and data typesTest data, URLs, timeoutsVariables, Data Types
Conditions and loopsData-driven tests, retriesConditions, Loops
Functions and async/awaitEvery test and helperFunctions
ArraysLists of elements and test casesArrays
ObjectsTest data, config, API payloadsObjects
Interfaces and typesTyped data and API responsesInterfaces
ClassesPage Object ModelClasses
GenericsFixtures, API helpersGenerics

πŸ’‘ Tip: If you are completely new, learn variables, functions, async/await, arrays and objects first. That is enough to write real Playwright tests. Add interfaces, classes and generics when you start building a framework.


βš™οΈ Step 1: Set Up a Playwright TypeScript Project

You need Node.js (LTS version) and VS Code. Then run one command in an empty folder:

npm init playwright@latest

Choose TypeScript when asked, accept the default tests folder, add a GitHub Actions workflow if you want CI, and let it install the browsers. Then install the official Playwright Test for VS Code extension to run and debug tests from the editor.

The generated project looks like this:

my-project/
β”œβ”€β”€ tests/
β”‚   └── example.spec.ts      # your tests
β”œβ”€β”€ playwright.config.ts     # Playwright settings (typed!)
β”œβ”€β”€ package.json
└── package-lock.json

πŸ› οΈ Step 2: Understand playwright.config.ts

The config file is plain TypeScript. defineConfig gives you autocomplete and type checking for every option.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,               // max time per test (ms)
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [['html', { open: 'never' }]],
  use: {
    baseURL: 'https://www.saucedemo.com',
    trace: 'on-first-retry',     // record a trace when a test is retried
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  ],
});

πŸ“ Note: Because of the types, a misspelled option such as retires: 2 is underlined in red immediately instead of being silently ignored.


✍️ Step 3: Your First TypeScript Test

// tests/login.spec.ts
import { test, expect } from '@playwright/test';

test('valid user can log in', async ({ page }) => {
  await page.goto('/');                                   // uses baseURL
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();

  await expect(page).toHaveURL(/inventory/);
  await expect(page.locator('.title')).toHaveText('Products');
});

Run it:

npx playwright test                 # run all tests headless
npx playwright test --ui            # interactive UI mode
npx playwright test login.spec.ts   # run one file
npx playwright show-report          # open the HTML report

⏳ async, await and Promise<T>

Almost every Playwright action talks to the browser, so it returns a Promise. Always put await in front, otherwise the next line runs before the action finishes.

// βœ… correct
await page.getByRole('button', { name: 'Login' }).click();

// ❌ missing await: the test may finish before the click happens
page.getByRole('button', { name: 'Login' }).click();

// Helper functions return Promise<T>
async function getCartCount(page: Page): Promise<number> {
  const text = await page.locator('.shopping_cart_badge').textContent();
  return Number(text ?? 0);
}

⚠️ Common mistake: A missing await is the number one cause of flaky Playwright tests. Enable the ESLint rule @typescript-eslint/no-floating-promises to catch it automatically.


🏷️ The Playwright Types You Will Use Most

Import these from @playwright/test to type your helpers and page objects:

TypeWhat it represents
PageA browser tab
LocatorA way to find element(s) on the page
BrowserContextAn isolated browser session (cookies, storage)
APIRequestContextThe client for API tests (request fixture)
TestInfoDetails about the running test (title, retries, attachments)
expectThe assertion function
import { Page, Locator, expect } from '@playwright/test';

async function addToCart(page: Page, productName: string): Promise<void> {
  const item: Locator = page.locator('.inventory_item').filter({ hasText: productName });
  await item.getByRole('button', { name: 'Add to cart' }).click();
  await expect(page.locator('.shopping_cart_badge')).toBeVisible();
}

πŸ“¦ Typing Test Data

Use interfaces and union types so wrong test data is impossible to write.

// test-data/users.ts
export type Role = 'standard' | 'locked' | 'problem';

export interface TestUser {
  username: string;
  password: string;
  canLogin: boolean;
}

export const USERS: Record<Role, TestUser> = {
  standard: { username: 'standard_user',   password: 'secret_sauce', canLogin: true },
  locked:   { username: 'locked_out_user', password: 'secret_sauce', canLogin: false },
  problem:  { username: 'problem_user',    password: 'secret_sauce', canLogin: true },
};
// tests/users.spec.ts
import { test, expect } from '@playwright/test';
import { USERS } from '../test-data/users';

for (const [role, user] of Object.entries(USERS)) {
  test('login behaviour for ' + role + ' user', async ({ page }) => {
    await page.goto('/');
    await page.getByPlaceholder('Username').fill(user.username);
    await page.getByPlaceholder('Password').fill(user.password);
    await page.getByRole('button', { name: 'Login' }).click();

    if (user.canLogin) {
      await expect(page).toHaveURL(/inventory/);
    } else {
      await expect(page.locator('[data-test="error"]')).toBeVisible();
    }
  });
}

πŸ›οΈ Page Objects with Classes

As tests grow, move locators and actions into classes. This is the Page Object Model:

// pages/InventoryPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class InventoryPage {
  readonly title: Locator;
  readonly cartBadge: Locator;

  constructor(private readonly page: Page) {
    this.title = page.locator('.title');
    this.cartBadge = page.locator('.shopping_cart_badge');
  }

  async addItem(name: string): Promise<void> {
    await this.page.locator('.inventory_item').filter({ hasText: name })
      .getByRole('button', { name: 'Add to cart' }).click();
  }

  async expectCartCount(count: number): Promise<void> {
    await expect(this.cartBadge).toHaveText(String(count));
  }
}

Full walkthrough: Playwright Page Object Model step by step.


🧩 Typed Fixtures with Generics

Custom fixtures inject ready-made page objects into every test. test.extend<T>() is a generic, so each fixture is fully typed.

// fixtures.ts
import { test as base } from '@playwright/test';
import { InventoryPage } from './pages/InventoryPage';

type Pages = { inventoryPage: InventoryPage };

export const test = base.extend<Pages>({
  inventoryPage: async ({ page }, use) => {
    await use(new InventoryPage(page));
  },
});
export { expect } from '@playwright/test';
// tests/cart.spec.ts
import { test } from '../fixtures';

test('add two items to the cart', async ({ page, inventoryPage }) => {
  // log in first (or reuse a saved session with storageState)
  await inventoryPage.addItem('Sauce Labs Backpack');
  await inventoryPage.addItem('Sauce Labs Bike Light');
  await inventoryPage.expectCartCount(2);
});

πŸ” Important: Playwright Does Not Type-Check Your Tests

Playwright transpiles TypeScript so it can run it fast, but it does not report type errors when you run npx playwright test. Your editor shows them, but a CI run can still pass with type errors. Add a type-check step:

npx tsc --noEmit

Add a minimal tsconfig.json if your project does not have one:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "noEmit": true
  },
  "include": ["tests", "pages", "test-data", "*.ts"]
}

πŸ’‘ Tip: Run npx tsc --noEmit before npx playwright test in your GitHub Actions pipeline so type errors fail the build early.


🐞 Common TypeScript Errors in Playwright (and Fixes)

Error messageCauseFix
Object is possibly ‘null’textContent() can return nullUse ?? '' or assert with toHaveText
Property ‘x’ does not exist on typeTypo or missing property in a typeFix the name or add it to the interface
‘await’ expressions are only allowed within async functionsMissing asyncAdd async before the function
Cannot find module ‘../pages/LoginPage’Wrong relative pathCheck the path and file name casing
Argument of type ‘string or undefined’Reading process.envProvide a default: process.env.BASE_URL ?? 'https://...'

πŸš€ Recommended Learning Path

  1. Learn the TypeScript basics from the table above (1 to 2 weeks).
  2. Write simple tests with locators and assertions: Locators, Assertions.
  3. Learn fixtures and annotations.
  4. Build a framework with the Page Object Model and external test data (JSON, CSV, .env).
  5. Complete the beginner automation project.
  6. Prepare with 200 Playwright + TypeScript interview questions.

❓ FAQ

πŸ€” Do I need to learn TypeScript before Playwright?

You need the basics: variables, functions, async/await, arrays and objects. You can learn interfaces, classes and generics while building your first framework.

πŸ€” Is TypeScript or JavaScript better for Playwright?

TypeScript is recommended. It runs with zero extra setup, gives autocomplete and catches mistakes before tests run. JavaScript works too, but large suites are harder to maintain.

πŸ€” How long does it take to learn TypeScript for Playwright?

Most testers with basic programming knowledge are productive in 2 to 4 weeks of regular practice, following the learning path above.

πŸ€” Does Playwright check TypeScript types when running tests?

No. Playwright only transpiles TypeScript. Run npx tsc --noEmit separately (locally and in CI) to catch type errors.

πŸ€” Which TypeScript version does Playwright need?

Playwright bundles its own TypeScript transpiler, so you do not need to install TypeScript to run tests. Install the typescript package only for tsc type checking and editor tooling.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

TypeScript Generics Explained for Beginners (with Examples and Playwright Use Cases)

Generics sound advanced, but the idea is simple: write code once and let it work safely with any type. This guide explains TypeScript generics step by step for beginners, with clear examples and the places you already meet them in Playwright.

πŸ“– What You’ll Learn: what generics are, generic functions and arrow functions, multiple type parameters, constraints with extends and keyof, generic interfaces and classes, utility types like Partial and Omit, and generics in Playwright fixtures and API helpers.


πŸ§ͺ What Are Generics? (Explained Simply)

Generics let you write code that works with many types while still keeping full type safety. You write the logic once and leave a “blank” for the type. The blank is filled in when the code is used.

A useful analogy is a labelled storage box. The box works the same whether it holds books or shoes, but once you write “Books” on the label, you can only put books in it and you know you will get books out. In TypeScript that label is the type parameter, usually written <T>.


πŸ€” The Problem Generics Solve

Imagine a function that returns the first item of an array. Without generics you have two bad options:

// Option 1: one function per type (repetitive)
function firstString(items: string[]): string { return items[0]; }
function firstNumber(items: number[]): number { return items[0]; }

// Option 2: use any (no type safety)
function firstAny(items: any[]): any { return items[0]; }

const value = firstAny(['chromium', 'firefox']);
value.toFixed(2); // ❌ no error in the editor, but crashes at runtime!

✨ Your First Generic Function

Add <T> after the function name. T is a placeholder for “whatever type the caller uses”.

function first<T>(items: T[]): T {
  return items[0];
}

const browser = first(['chromium', 'firefox']); // T is string, so browser: string
const score = first([90, 85, 70]);              // T is number, so score: number

browser.toUpperCase(); // βœ… string method allowed
// browser.toFixed(2); // ❌ Error: toFixed does not exist on type string

TypeScript usually infers T from the arguments. You can also pass it explicitly: first<string>(['a', 'b']).

πŸ’‘ Tip: T is just a convention (for “Type”). Other common names are K (key), V (value) and E (element). Descriptive names like TData are fine too.


➑️ Generic Arrow Functions

Arrow functions use the same idea; the <T> goes before the parameters.

const last = <T>(items: T[]): T => items[items.length - 1];

const wrapInArray = <T>(value: T): T[] => [value];

last(['login', 'checkout', 'logout']); // 'logout'
wrapInArray(42);                       // [42]

πŸ“ Note: In .tsx files, write <T,> (with a comma) so the compiler does not confuse it with a JSX tag.


πŸ”’ Multiple Type Parameters

function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

const p1 = pair('timeout', 30000); // [string, number]
const p2 = pair(1, true);          // [number, boolean]

function mapValues<T, R>(items: T[], fn: (item: T) => R): R[] {
  return items.map(fn);
}

const lengths = mapValues(['a', 'abc'], s => s.length); // number[]

🚧 Generic Constraints with extends

Sometimes T should not be “anything”. A constraint says “T must have at least these properties”. Use extends for that.

// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest('playwright', 'ts');  // βœ… strings have length
longest([1, 2, 3], [1]);      // βœ… arrays have length
// longest(10, 20);           // ❌ Error: number has no length

A very common constraint is K extends keyof T, which means “K must be one of the property names of T”.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Deepesh', role: 'SDET', years: 8 };

getProperty(user, 'name');   // string
getProperty(user, 'years');  // number
// getProperty(user, 'email'); // ❌ Error: 'email' is not a key of user

πŸ“¦ Generic Interfaces and Types

Generics are especially powerful in interfaces. A classic example is a wrapper for API responses where only the data part changes.

interface ApiResponse<T> {
  status: number;
  message: string;
  data: T;
}

interface User { id: number; name: string }
interface Product { sku: string; price: number }

const userRes: ApiResponse<User> = {
  status: 200, message: 'OK', data: { id: 1, name: 'Asha' },
};

const productsRes: ApiResponse<Product[]> = {
  status: 200, message: 'OK', data: [{ sku: 'LP-01', price: 55000 }],
};

// Default type parameter
type Paginated<T = string> = { page: number; items: T[] };
const tags: Paginated = { page: 1, items: ['smoke', 'regression'] };

πŸ›οΈ Generic Classes

Classes can be generic too. This simple typed store keeps test data of one type only.

class DataStore<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    return [...this.items];
  }

  find(predicate: (item: T) => boolean): T | undefined {
    return this.items.find(predicate);
  }
}

const users = new DataStore<{ email: string; active: boolean }>();
users.add({ email: 'a@test.com', active: true });
// users.add('hello'); // ❌ Error: string is not the stored type

const active = users.find(u => u.active);

🧰 Built-in Generic Utility Types

TypeScript ships with ready-made generics you will use every day:

UtilityWhat it doesExample
Array<T>Array of TArray<string>
Promise<T>Async result of TPromise<void>
Partial<T>All properties optionalOverride objects
Required<T>All properties requiredFinal config
Readonly<T>All properties read-onlyConstant test data
Pick<T, K>Keep only some propertiesPick<User, 'name'>
Omit<T, K>Remove some propertiesOmit<User, 'password'>
Record<K, V>Object with keys K and values VRecord<string, number>
interface User { id: number; name: string; email: string; password: string }

type PublicUser = Omit<User, 'password'>;        // no password
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;

const patch: UserUpdate = { email: 'new@test.com' };

🎭 Generics in Playwright: Real Examples

You already use generics in Playwright, maybe without noticing: every async method returns a Promise<T>, and test.extend<T>() is a generic.

1. A typed API helper

import { APIRequestContext, expect } from '@playwright/test';

export async function getJson<T>(request: APIRequestContext, url: string): Promise<T> {
  const response = await request.get(url);
  expect(response.ok()).toBeTruthy();
  return (await response.json()) as T;
}

// usage in a test
interface Todo { id: number; title: string; completed: boolean }

const todo = await getJson<Todo>(request, 'https://jsonplaceholder.typicode.com/todos/1');
expect(todo.completed).toBe(false); // todo is fully typed

2. Typed custom fixtures with test.extend<T>()

import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

type MyFixtures = {
  loginPage: LoginPage;
  testUser: { username: string; password: string };
};

export const test = base.extend<MyFixtures>({
  testUser: async ({}, use) => {
    await use({ username: 'standard_user', password: 'secret_sauce' });
  },
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

Learn more in Playwright Custom Fixtures with Examples.

3. A generic test data loader

import fs from 'fs';

export function loadJson<T>(path: string): T {
  return JSON.parse(fs.readFileSync(path, 'utf-8')) as T;
}

const users = loadJson<{ username: string; password: string }[]>('test-data/users.json');

See the full approach in Read JSON data in Playwright.


βœ… Best Practices

  • Use generics when the same logic works for different types; do not add <T> when one type is enough.
  • Let TypeScript infer T whenever possible; pass it explicitly when inference cannot know it (for example API responses).
  • Add constraints (extends) so the function only accepts types it can really handle.
  • Prefer generics over any. any switches off type checking; generics keep it on.
  • Remember that as T is a promise you make to the compiler; validate important API responses with assertions.

πŸ‹οΈ Practice Exercises

  1. Write a generic function lastItem<T>(arr: T[]): T | undefined.
  2. Write toMap<T, K extends keyof T>(items: T[], key: K) that returns a Map keyed by that property.
  3. Create interface Result<T> { ok: boolean; value?: T; error?: string } and a function that returns it.
  4. Build a generic Queue<T> class with enqueue, dequeue and size.

❓ FAQ

πŸ€” What are generics in TypeScript in simple terms?

A way to write a function, interface or class once and use it with any type, while TypeScript still tracks exactly which type is used each time.

πŸ€” What does <T> mean in TypeScript?

T is a type parameter: a placeholder name for a type that will be provided (or inferred) when the code is used.

πŸ€” What is the difference between generics and any?

any turns off type checking. Generics keep type checking: if you pass strings in, TypeScript knows you get strings out.

πŸ€” What does T extends mean in generics?

It is a constraint. T extends { id: number } means T can be any type, as long as it has a numeric id property.

πŸ€” Where are generics used in Playwright?

In Promise<T> return types, test.extend<Fixtures>() for custom fixtures, and typed helpers such as a getJson<T>() API function.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript generics programs with solutions →

TypeScript Classes for Beginners: Constructors, Inheritance, Access Modifiers and Page Objects

Classes are the heart of object-oriented programming and the reason Playwright frameworks stay clean as they grow. This beginner guide explains TypeScript classes in plain language, from your first constructor to a working Page Object Model.

πŸ“– What You’ll Learn: class syntax, constructors, parameter properties, public/private/protected/readonly, inheritance with extends and super, getters and setters, static members, abstract classes, implementing interfaces, and Page Object Model classes in Playwright.


πŸ—οΈ What Is a Class?

A class is a template for creating objects. It bundles data (properties) and behavior (methods) together. Each object created from a class is called an instance.

A simple analogy: a class is the architect’s plan for a house, and each house built from that plan is an object. Every house has the same rooms (properties) and the same doors and switches (methods), but each one can have different paint and furniture (values).

In Playwright, classes are the foundation of the Page Object Model: each page of your application becomes a class with locators as properties and user actions as methods.


✍️ Creating Your First Class

class Car {
  brand: string;
  speed: number;

  constructor(brand: string, speed: number) {
    this.brand = brand;
    this.speed = speed;
  }

  describe(): string {
    return this.brand + ' runs at ' + this.speed + ' km/h';
  }
}

const car1 = new Car('Tata', 120);   // create an instance (object)
const car2 = new Car('Honda', 140);

console.log(car1.describe()); // Tata runs at 120 km/h
console.log(car2.brand);      // Honda

Key words to remember:

  • class declares the template.
  • constructor runs once when you call new and sets up the starting values.
  • this refers to the current object.
  • new creates a fresh object from the class.

⚠️ Common mistake: Forgetting this. inside a method is the most common beginner error. Write this.speed, not speed, to read the object’s own property.


⚑ Parameter Properties: The Short Way

TypeScript lets you declare and assign properties directly in the constructor by adding an access modifier (public, private, protected or readonly) before the parameter. You will see this in almost every Playwright page object.

// Long form
class UserLong {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// Short form: same result, less code
class User {
  constructor(public name: string, private password: string) {}
}

const u = new User('admin', 'secret');
console.log(u.name);       // admin
// console.log(u.password); // ❌ Error: 'password' is private

πŸ” Access Modifiers: public, private, protected, readonly

Access modifiers control who can use a property or method.

ModifierAccessible fromTypical use
public (default)AnywhereMethods tests call, like login()
privateOnly inside the same classInternal helpers and secrets
protectedThe class and its child classesShared helpers in a base page
readonlyRead anywhere, set only in the constructorLocators, URLs
class BankAccount {
  readonly accountNumber: string;
  private balance = 0;

  constructor(accountNumber: string) {
    this.accountNumber = accountNumber;
  }

  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Amount must be positive');
    this.balance += amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

const acc = new BankAccount('SB-1001');
acc.deposit(500);
console.log(acc.getBalance()); // 500
// acc.balance = 1000000;      // ❌ Error: private

🧬 Inheritance with extends and super

Inheritance lets one class reuse another class. The child class gets every property and method of the parent and can add or override its own. Call super() in the child constructor to run the parent constructor first.

class Employee {
  constructor(public name: string, protected salary: number) {}

  getDetails(): string {
    return this.name + ' earns ' + this.salary;
  }
}

class Manager extends Employee {
  constructor(name: string, salary: number, public teamSize: number) {
    super(name, salary); // must come first
  }

  // Override the parent method
  getDetails(): string {
    return super.getDetails() + ' and manages ' + this.teamSize + ' people';
  }
}

const m = new Manager('Neha', 150000, 8);
console.log(m.getDetails()); // Neha earns 150000 and manages 8 people

🧰 Getters, Setters and Static Members

Getters and setters look like properties but run code. Static members belong to the class itself, not to each object. They are handy for shared constants and utility methods.

class Temperature {
  private celsius = 0;

  get fahrenheit(): number {
    return this.celsius * 9 / 5 + 32;
  }

  set value(c: number) {
    if (c < -273.15) throw new Error('Below absolute zero');
    this.celsius = c;
  }
}

const t = new Temperature();
t.value = 25;
console.log(t.fahrenheit); // 77

class TestUtils {
  static readonly DEFAULT_TIMEOUT = 30000;

  static randomEmail(): string {
    return 'user' + Date.now() + '@test.com';
  }
}

console.log(TestUtils.DEFAULT_TIMEOUT); // no "new" needed
console.log(TestUtils.randomEmail());

🧱 Abstract Classes

An abstract class cannot be created directly with new. It is a base for other classes and can force them to implement certain methods. This is useful for a BasePage that every page must extend.

abstract class Shape {
  abstract area(): number; // child classes MUST implement this

  describe(): string {
    return 'Area is ' + this.area().toFixed(2);
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super(); }
  area(): number { return Math.PI * this.radius ** 2; }
}

console.log(new Circle(2).describe()); // Area is 12.57
// new Shape(); // ❌ Error: cannot create an instance of an abstract class

🀝 Classes and Interfaces Together

A class can implement an interface. The interface describes *what* must exist and the class provides *how* it works. If the class misses a member, TypeScript reports an error.

interface Reporter {
  report(testName: string, passed: boolean): void;
}

class ConsoleReporter implements Reporter {
  report(testName: string, passed: boolean): void {
    console.log((passed ? 'βœ… ' : '❌ ') + testName);
  }
}

Classes vs interfaces in one line: an interface is a compile-time contract with no code, while a class is real code that creates objects at runtime.


🎭 Classes in Playwright: Page Object Model

Here is a small but realistic Page Object Model built with everything you learned: readonly locators, a constructor, a protected base helper, inheritance and public action methods.

1. Base page

// pages/BasePage.ts
import { Page } from '@playwright/test';

export abstract class BasePage {
  constructor(protected readonly page: Page) {}

  abstract readonly path: string;

  async open(): Promise<void> {
    await this.page.goto('https://www.saucedemo.com' + this.path);
  }
}

2. Login page

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  readonly path = '/';
  readonly username: Locator;
  readonly password: Locator;
  readonly loginButton: Locator;
  readonly error: Locator;

  constructor(page: Page) {
    super(page);
    this.username = page.getByPlaceholder('Username');
    this.password = page.getByPlaceholder('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
    this.error = page.locator('[data-test="error"]');
  }

  async login(user: string, pass: string): Promise<void> {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.loginButton.click();
  }
}

3. The test stays short and readable

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('valid user can log in', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.open();
  await loginPage.login('standard_user', 'secret_sauce');
  await expect(page).toHaveURL(/inventory/);
});

test('locked user sees an error', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.open();
  await loginPage.login('locked_out_user', 'secret_sauce');
  await expect(loginPage.error).toContainText('locked out');
});

Go deeper with Playwright Page Object Model step by step and Playwright Custom Fixtures, which can create page objects for you automatically.


βœ… Best Practices

  • One class per page or component; keep each class focused on one responsibility (see SOLID principles with Playwright).
  • Mark locators readonly so they cannot be reassigned by accident.
  • Keep assertions in tests, and keep actions in page classes.
  • Use private for helpers the tests should not call directly.
  • Prefer composition (a page that *has* a header component) over deep inheritance chains.

❓ FAQ

πŸ€” What is the difference between a class and an object in TypeScript?

A class is the template; an object is a real value created from it with new. One class can create many objects.

πŸ€” What is a constructor in TypeScript?

A special method named constructor that runs automatically when you create an object with new. It sets the starting values of properties.

πŸ€” When should I use a class instead of an interface?

Use a class when you need real behavior and to create objects at runtime, such as page objects. Use an interface when you only need to describe a shape or contract.

πŸ€” Why do Playwright page objects use classes?

Classes group a page’s locators and actions in one place, so tests read like user steps and a UI change only needs one fix in the page class.

πŸ€” What does super() do?

In a child class constructor, super() calls the parent class constructor. It must run before you use this.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript class programs with solutions →

TypeScript Interfaces Explained: Examples, Interface vs Type, and Playwright Use Cases

Interfaces are how TypeScript describes the shape of your data. In test automation they keep test data, API responses and page objects consistent, so typos are caught in the editor instead of in a failed CI run. This guide explains interfaces from scratch with simple examples.

πŸ“– What You’ll Learn: interface syntax, optional and readonly properties, default values, methods in interfaces, extends, index signatures, interface vs type, interface vs class, and how to use interfaces in Playwright tests.


πŸ“ What Is an Interface in TypeScript?

An interface is a named description of what an object must look like: which properties it has, what type each one is, and which methods it provides. Think of it as a contract or a blueprint. Any object that claims to follow the interface must match it, or TypeScript shows an error before your code runs.

Interfaces do not exist at runtime. They are removed when TypeScript compiles to JavaScript, so they add safety without adding any code to your tests.

interface User {
  username: string;
  password: string;
  age: number;
}

const tester: User = {
  username: 'standard_user',
  password: 'secret_sauce',
  age: 28,
};

// ❌ Error: Property 'age' is missing in type
// const broken: User = { username: 'x', password: 'y' };

πŸ’‘ Tip: By convention, interface names use PascalCase (User, LoginPage, ApiResponse). Do not add an I prefix like IUser; the TypeScript team recommends against it.


🧩 Interface Example: Optional and Readonly Properties

Just like object types, interface properties can be optional (?) or readonly.

interface TestEnvironment {
  readonly name: string;    // cannot change after creation
  baseUrl: string;
  apiKey?: string;          // optional
  timeout?: number;         // optional
}

const qa: TestEnvironment = { name: 'QA', baseUrl: 'https://qa.sqatools.in' };

qa.baseUrl = 'https://qa2.sqatools.in'; // βœ…
// qa.name = 'PROD';                    // ❌ read-only

🎚️ Default Values for Interface Properties

A common beginner question is “how do I give an interface a default value?”. Interfaces only describe types, so they cannot hold values. Instead, set defaults where the object is created, usually with the spread operator or default parameters.

interface BrowserOptions {
  browser: 'chromium' | 'firefox' | 'webkit';
  headless?: boolean;
  slowMo?: number;
}

const defaultOptions: Required<BrowserOptions> = {
  browser: 'chromium',
  headless: true,
  slowMo: 0,
};

function createOptions(custom: Partial<BrowserOptions> = {}): Required<BrowserOptions> {
  return { ...defaultOptions, ...custom };
}

createOptions({ headless: false });
// { browser: 'chromium', headless: false, slowMo: 0 }

πŸ“ Note: Partial<T> makes every property optional and Required<T> makes every property required. They are built-in utility types that work with any interface.


πŸ› οΈ Methods and Functions in Interfaces

Interfaces can describe methods too. This is exactly how you describe what a page object must be able to do.

interface LoginActions {
  open(): Promise<void>;
  login(username: string, password: string): Promise<void>;
  getErrorMessage(): Promise<string>;
}

// An interface can also describe a standalone function type
interface Formatter {
  (value: number): string;
}

const toPrice: Formatter = (value) => 'β‚Ή' + value.toFixed(2);
console.log(toPrice(499)); // β‚Ή499.00

πŸ”— Extending Interfaces

Interfaces can extend other interfaces to reuse properties. The new interface gets everything from the parent plus its own properties. You can extend more than one interface at once.

interface Person {
  firstName: string;
  lastName: string;
}

interface Employee extends Person {
  employeeId: number;
  department: string;
}

interface Timestamps {
  createdAt: Date;
}

interface Admin extends Employee, Timestamps {
  permissions: string[];
}

const lead: Admin = {
  firstName: 'Ravi',
  lastName: 'Kumar',
  employeeId: 101,
  department: 'QA',
  createdAt: new Date(),
  permissions: ['create-user', 'delete-user'],
};

πŸ“š Interfaces for Arrays, Nested Data and Dynamic Keys

interface Address { city: string; zip: string }

interface Customer {
  id: number;
  name: string;
  address: Address;          // nested interface
  tags: string[];            // array of strings
  orders: { id: number; total: number }[]; // array of objects
}

// Index signature: any number of string keys
interface Headers {
  [header: string]: string;
}

const headers: Headers = { 'Content-Type': 'application/json', Accept: '*/*' };

βš”οΈ Interface vs Type: What Is the Difference?

This is one of the most searched TypeScript questions. For describing objects, both work and look almost the same. The differences:

Featureinterfacetype
Describe object shapesβœ… Yesβœ… Yes
Extend / reuseextends keywordIntersection &
Union types ('a' | 'b')❌ Noβœ… Yes
Primitives and tuples❌ Noβœ… Yes
Declaration merging (same name twice)βœ… Merges❌ Error
Implemented by classesβœ… Yesβœ… Yes (object types)
// Same shape, two ways
interface UserA { name: string }
type UserB = { name: string };

// Extending
interface AdminA extends UserA { role: string }
type AdminB = UserB & { role: string };

// Only a type can do this
type Browser = 'chromium' | 'firefox' | 'webkit';
type Point = [number, number];

// Only an interface can do this (declaration merging)
interface Settings { theme: string }
interface Settings { language: string }
const s: Settings = { theme: 'dark', language: 'en' };

πŸ’‘ Tip: A simple rule for beginners: use interface for objects and class contracts (page objects, API models), and use type for unions, tuples and combinations. Be consistent within your project.


πŸ›οΈ Interface vs Class

An interface only describes shape and is removed at compile time. A class creates real objects at runtime and can contain code, constructors and default values. A class can implement an interface to promise it follows that contract.

interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log('[test] ' + message);
  }
}

// ❌ Error if the class forgets the log() method

🎭 Interfaces in Playwright: Real Examples

1. Typing test data

// test-data/users.ts
export interface UserData {
  username: string;
  password: string;
  expectedPage: string;
}

export const users: Record<'standard' | 'problem', UserData> = {
  standard: { username: 'standard_user', password: 'secret_sauce', expectedPage: '/inventory.html' },
  problem:  { username: 'problem_user',  password: 'secret_sauce', expectedPage: '/inventory.html' },
};

2. Typing an API response

import { test, expect } from '@playwright/test';

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

test('GET a post returns the correct shape', async ({ request }) => {
  const response = await request.get('https://jsonplaceholder.typicode.com/posts/1');
  expect(response.ok()).toBeTruthy();

  const post: Post = await response.json();
  expect(post.id).toBe(1);
  expect(typeof post.title).toBe('string'); // editor autocompletes post.title
});

3. A contract for page objects

import { Page } from '@playwright/test';

interface BasePage {
  readonly page: Page;
  readonly url: string;
  open(): Promise<void>;
}

export class LoginPage implements BasePage {
  readonly url = 'https://www.saucedemo.com/';
  constructor(readonly page: Page) {}

  async open() {
    await this.page.goto(this.url);
  }

  async login(user: { username: string; password: string }) {
    await this.page.getByPlaceholder('Username').fill(user.username);
    await this.page.getByPlaceholder('Password').fill(user.password);
    await this.page.getByRole('button', { name: 'Login' }).click();
  }
}

Read the full pattern in Playwright Page Object Model step by step.


βœ… Best Practices

  • Name interfaces after what they represent: UserData, ApiResponse, CheckoutPage.
  • Keep shared interfaces in one folder (for example types/) and export them.
  • Use extends to build specific interfaces from general ones instead of copy-pasting properties.
  • Type every API response you assert on. Autocomplete then prevents typos in property names.
  • Prefer Partial<T> for “update” or “override” objects instead of making every property optional.

❓ FAQ

πŸ€” What is an interface in TypeScript in simple words?

It is a named blueprint that lists the properties and methods an object must have. TypeScript checks every object against it while you write code.

πŸ€” Should I use interface or type in TypeScript?

Both work for objects. Use interface for object shapes and class contracts, and type for unions, tuples and other combinations. Pick one style per project and stay consistent.

πŸ€” Can a TypeScript interface have default values?

No. Interfaces only describe types. Set defaults when creating the object, for example const opts = { ...defaults, ...custom }.

πŸ€” Can an interface extend multiple interfaces?

Yes: interface Admin extends Employee, Timestamps {} combines all properties from both parents.

πŸ€” Do interfaces slow down my tests?

No. Interfaces are removed during compilation and produce zero JavaScript, so they have no runtime cost.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript interface programs with solutions →

TypeScript Objects: Complete Beginner Guide with Playwright Examples

Objects are the backbone of test automation: login credentials, form data, API payloads and even your Playwright config are all objects. In this beginner-friendly guide you will learn how TypeScript objects work, how to give them types, and how to use them in real Playwright tests.

πŸ“– What You’ll Learn: object types, type aliases, optional and readonly properties, index signatures and Record, nested objects, arrays of objects, looping, destructuring, merging, comparing objects, and objects in Playwright tests.


🧱 What Is an Object in TypeScript?

An object is a way to group related values together under one name. Instead of creating five separate variables for a user’s name, email, password, role and age, you put them in one object. Each value is stored as a property: a key (the name) and a value.

In plain JavaScript, objects can hold anything and you only find mistakes when the code runs. TypeScript adds types to objects, so your editor warns you the moment you misspell a property or put a number where a string belongs. For test automation this matters a lot, because most test data, such as login users, form inputs and API payloads, is stored in objects.

// A simple object: TypeScript infers the type automatically
const user = {
  name: 'Deepesh',
  email: 'deepesh@sqatools.in',
  age: 30,
  isActive: true,
};

console.log(user.name);   // Deepesh
console.log(user['email']); // deepesh@sqatools.in (bracket notation)

πŸ“ Note: When you hover over user in VS Code, TypeScript shows the inferred type { name: string; email: string; age: number; isActive: boolean }. You did not write it, but TypeScript still protects you.


✍️ Declaring an Object Type

You can describe the shape of an object explicitly. This is called an object type annotation. It lists every property and its type, separated by semicolons.

let loginUser: { username: string; password: string; rememberMe: boolean };

loginUser = {
  username: 'standard_user',
  password: 'secret_sauce',
  rememberMe: false,
};

// ❌ Error: Property 'rememberMe' is missing
// loginUser = { username: 'admin', password: '1234' };

// ❌ Error: Type 'number' is not assignable to type 'string'
// loginUser = { username: 101, password: 'x', rememberMe: true };

Writing the shape inline works, but it gets repetitive. In real projects you give the shape a name with a type alias (or an interface) and reuse it everywhere.

type User = {
  username: string;
  password: string;
  role: string;
};

const admin: User = { username: 'admin', password: 'Admin@123', role: 'admin' };
const guest: User = { username: 'guest', password: 'Guest@123', role: 'viewer' };

❓ Optional and Readonly Properties

Not every property is always present. Add a ? after the name to make it optional. Add readonly in front to stop anyone from changing it after the object is created.

type TestConfig = {
  readonly baseUrl: string; // cannot be changed later
  browser: string;
  headless?: boolean;       // optional
  retries?: number;         // optional
};

const config: TestConfig = {
  baseUrl: 'https://sqatools.in',
  browser: 'chromium',
};

config.browser = 'firefox';   // βœ… allowed
// config.baseUrl = 'https://x.com'; // ❌ Error: read-only property

// Optional values may be undefined, so give a fallback
const retries = config.retries ?? 0;

πŸ’‘ Tip: Use readonly for values like URLs and environment names that should never change during a test run. It prevents accidental overwrites in shared test data.


πŸ”§ Adding, Updating and Deleting Properties

TypeScript only lets you use properties that exist in the type. This is a feature: it catches typos like usernmae immediately.

type Product = { name: string; price: number; inStock?: boolean };

const product: Product = { name: 'Laptop', price: 55000 };

product.price = 52000;     // βœ… update
product.inStock = true;    // βœ… add an optional property
delete product.inStock;    // βœ… delete works only on optional properties

// product.color = 'black'; // ❌ Error: 'color' does not exist on type 'Product'

If you genuinely need an object whose keys are not known in advance (for example, HTTP headers), use an index signature or the built-in Record type.

// Any string key, every value is a string
const headers: { [key: string]: string } = {};
headers['Content-Type'] = 'application/json';
headers['Authorization'] = 'Bearer token123';

// Same thing using Record<KeyType, ValueType>
const statusMessages: Record<number, string> = {
  200: 'OK',
  404: 'Not Found',
  500: 'Server Error',
};

πŸͺ† Nested Objects

Objects can contain other objects. This is common for API responses and page configuration.

type Address = { city: string; pincode: string };

type Customer = {
  id: number;
  name: string;
  address: Address;          // nested object
  contacts: { email: string; phone?: string };
};

const customer: Customer = {
  id: 1,
  name: 'Asha',
  address: { city: 'Pune', pincode: '411001' },
  contacts: { email: 'asha@test.com' },
};

console.log(customer.address.city);      // Pune
console.log(customer.contacts.phone?.length); // undefined (optional chaining)

⚠️ Common mistake: Accessing a property of something that may be undefined crashes at runtime. Use optional chaining ?. to read safely, as in customer.contacts.phone?.length.


πŸ“‹ Arrays of Objects

An array of objects is the most common way to store test data for data-driven tests. Each object is one test case.

type LoginCase = { title: string; username: string; password: string; expectError: boolean };

const loginCases: LoginCase[] = [
  { title: 'valid user',      username: 'standard_user',  password: 'secret_sauce', expectError: false },
  { title: 'locked user',     username: 'locked_out_user', password: 'secret_sauce', expectError: true },
  { title: 'wrong password',  username: 'standard_user',  password: 'wrong',        expectError: true },
];

// Find, filter and map work exactly like normal arrays
const negativeCases = loginCases.filter(c => c.expectError);
const titles = loginCases.map(c => c.title);
console.log(titles); // ['valid user', 'locked user', 'wrong password']

Learn more array methods in our TypeScript Arrays guide.


πŸ” Looping Through an Object

Use Object.keys(), Object.values() and Object.entries() to work with every property.

const scores = { chromium: 98, firefox: 95, webkit: 91 };

Object.keys(scores);    // ['chromium', 'firefox', 'webkit']
Object.values(scores);  // [98, 95, 91]

for (const [browser, score] of Object.entries(scores)) {
  console.log(browser + ' scored ' + score);
}

🧩 Destructuring, Spread and Merging Objects

Destructuring pulls properties out into variables. The spread operator ... copies properties into a new object, which is the easiest way to merge or override values.

const user = { username: 'admin', password: 'Admin@123', role: 'admin' };

// Destructuring
const { username, password } = user;

// Merge two objects (later values win)
const defaults = { browser: 'chromium', headless: true, retries: 0 };
const overrides = { headless: false, retries: 2 };
const finalConfig = { ...defaults, ...overrides };
// { browser: 'chromium', headless: false, retries: 2 }

// Copy and change one field without touching the original
const guest = { ...user, username: 'guest', role: 'viewer' };

⚠️ Common mistake: Spread makes a shallow copy. Nested objects are still shared between the copy and the original. For a deep copy use structuredClone(obj).


βš–οΈ Comparing Objects

Two objects are only === equal if they are the same object in memory, even if their contents match. To compare contents, compare the values or use a deep equality check.

const a = { id: 1, name: 'Test' };
const b = { id: 1, name: 'Test' };

console.log(a === b); // false: different objects
console.log(JSON.stringify(a) === JSON.stringify(b)); // true (simple objects, same key order)

In Playwright tests you do not need any of this: expect(actual).toEqual(expected) performs a deep comparison for you.


🎭 Objects in Playwright: Real Examples

Objects are everywhere in Playwright. The config file is an object, test data is usually objects, and API requests send objects as JSON.

1. Test data object for a login test

import { test, expect } from '@playwright/test';

type Credentials = { username: string; password: string };

const validUser: Credentials = { username: 'standard_user', password: 'secret_sauce' };

test('user can log in', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill(validUser.username);
  await page.getByPlaceholder('Password').fill(validUser.password);
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL(/inventory/);
});

2. Data-driven tests from an array of objects

const cases = [
  { username: 'locked_out_user', error: 'Sorry, this user has been locked out.' },
  { username: 'unknown_user',    error: 'Username and password do not match' },
];

for (const c of cases) {
  test('shows error for ' + c.username, async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
    await page.getByPlaceholder('Username').fill(c.username);
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]')).toContainText(c.error);
  });
}

3. Sending and checking an object in an API test

test('create a post via API', async ({ request }) => {
  const payload = { title: 'Playwright', body: 'Objects in TypeScript', userId: 1 };

  const response = await request.post('https://jsonplaceholder.typicode.com/posts', { data: payload });
  expect(response.status()).toBe(201);

  const body = await response.json();
  expect(body).toMatchObject(payload); // deep, partial object comparison
});

βœ… Best Practices

  • Give every reusable object shape a name with type or interface instead of repeating inline types.
  • Mark values that must not change as readonly.
  • Use optional properties ? only when a value can truly be missing, and handle undefined with ?? or ?..
  • Keep test data objects in a separate file (for example test-data/users.ts) so tests stay short.
  • Prefer toEqual or toMatchObject in Playwright assertions to compare objects.

❓ FAQ

πŸ€” What is the difference between an object type and an interface in TypeScript?

Both describe the shape of an object and are often interchangeable. Interfaces can be extended and merged, while type aliases can also describe unions and primitives. Read the full comparison in TypeScript Interfaces.

πŸ€” How do I define an array of objects in TypeScript?

Create a type for one item, then add []: type User = { name: string }; const users: User[] = [];. You can also write Array<User>.

πŸ€” How do I merge two objects in TypeScript?

Use the spread operator: const merged = { ...first, ...second };. Properties in second overwrite the same properties in first.

πŸ€” How do I make an object property optional?

Add a question mark after the property name, for example phone?: string. Its value can then be a string or undefined.

πŸ€” Why does TypeScript say “Property does not exist on type”?

You are using a property that is not part of the object’s type, usually because of a typo or a missing field in the type. Add it to the type (optionally with ?) or fix the spelling.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 31 TypeScript object programs with solutions →

TypeScript Arrays β€” Complete Guide with map, filter, reduce & Playwright Examples

Arrays are everywhere in test automation β€” storing test data, collecting page elements, managing results. TypeScript arrays come with strong typing that catches bugs before your tests even run. This guide covers everything from basics to advanced patterns with Playwright examples.

πŸ“– What You’ll Learn: Array types, push/pop/splice, map/filter/reduce, spread operator, tuples, readonly arrays, and array operations in Playwright tests.


πŸ“¦ Declaring Arrays

// Two ways to declare typed arrays
let browsers: string[] = ['chromium', 'firefox', 'webkit'];
let scores: Array<number> = [85, 92, 78, 95];

// Mixed types with union
let mixed: (string | number)[] = ['hello', 42, 'world'];

// Empty array with type
let results: boolean[] = [];

// Array of objects
let users: { name: string; role: string }[] = [
  { name: 'Deepesh', role: 'SDET' },
  { name: 'John', role: 'QA Lead' },
];

// 2D array (matrix)
let grid: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

βž• Adding & Removing Elements

let fruits: string[] = ['apple', 'banana'];

// Add to end
fruits.push('cherry');           // ['apple', 'banana', 'cherry']

// Add to beginning
fruits.unshift('mango');         // ['mango', 'apple', 'banana', 'cherry']

// Remove from end
let last = fruits.pop();         // 'cherry', array: ['mango', 'apple', 'banana']

// Remove from beginning
let first = fruits.shift();      // 'mango', array: ['apple', 'banana']

// Remove/insert at position with splice
let colors: string[] = ['red', 'green', 'blue', 'yellow'];
colors.splice(1, 2);             // Remove 2 items from index 1 β†’ ['red', 'yellow']
colors.splice(1, 0, 'purple');   // Insert at index 1 β†’ ['red', 'purple', 'yellow']

// Find index and remove
let items = ['a', 'b', 'c', 'd'];
let idx = items.indexOf('c');
if (idx !== -1) items.splice(idx, 1);  // ['a', 'b', 'd']

πŸ” Searching & Checking Arrays

let numbers: number[] = [10, 20, 30, 40, 50];

// includes β€” does it contain a value?
console.log(numbers.includes(30));  // true

// indexOf β€” find position (-1 if not found)
console.log(numbers.indexOf(40));   // 3

// find β€” first element matching condition
let found = numbers.find(n => n > 25);
console.log(found);  // 30

// findIndex β€” index of first match
let foundIdx = numbers.findIndex(n => n > 25);
console.log(foundIdx);  // 2

// every β€” do ALL elements match?
let allPositive = numbers.every(n => n > 0);    // true

// some β€” does ANY element match?
let hasLarge = numbers.some(n => n > 100);       // false

πŸ”„ map, filter & reduce

let prices: number[] = [100, 200, 300, 400, 500];

// map β€” transform each element
let discounted = prices.map(p => p * 0.9);
console.log(discounted);  // [90, 180, 270, 360, 450]

// filter β€” keep elements matching condition
let expensive = prices.filter(p => p >= 300);
console.log(expensive);  // [300, 400, 500]

// reduce β€” combine all elements into one value
let total = prices.reduce((sum, p) => sum + p, 0);
console.log(total);  // 1500

// Chaining β€” filter + map + reduce
let premiumTotal = prices
  .filter(p => p >= 300)           // [300, 400, 500]
  .map(p => p * 1.1)               // [330, 440, 550]
  .reduce((sum, p) => sum + p, 0); // 1320
console.log(premiumTotal);

// flat β€” flatten nested arrays
let nested: number[][] = [[1, 2], [3, 4], [5]];
let flat = nested.flat();
console.log(flat);  // [1, 2, 3, 4, 5]

πŸ’‘ map transforms, filter selects, reduce combines. Chain them together for powerful data pipelines. These are the three most important array methods to master.


πŸ“Œ Spread, Destructuring & Tuples

// Spread operator β€” copy and merge arrays
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let combined = [...arr1, ...arr2];    // [1, 2, 3, 4, 5, 6]
let copy = [...arr1];                 // [1, 2, 3] β€” independent copy

// Destructuring β€” extract values
let [first, second, ...rest] = [10, 20, 30, 40, 50];
console.log(first);  // 10
console.log(rest);   // [30, 40, 50]

// Tuple β€” fixed-length, typed array
let coord: [number, number] = [40.7128, -74.0060];
let entry: [string, number, boolean] = ['test-1', 95, true];

// Readonly array β€” cannot be modified
const readonlyBrowsers: readonly string[] = ['chromium', 'firefox', 'webkit'];
// readonlyBrowsers.push('edge');  // ❌ Error! Cannot modify

// Sorting
let nums = [30, 10, 50, 20, 40];
nums.sort((a, b) => a - b);          // [10, 20, 30, 40, 50] ascending
nums.sort((a, b) => b - a);          // [50, 40, 30, 20, 10] descending

🎭 Arrays in Playwright β€” Real Examples

import { test, expect } from '@playwright/test';

test('collect and verify all product names', async ({ page }) => {
  await page.goto('https://sqatools.in');
  
  // Collect all text content into an array
  const links = page.locator('nav a');
  const texts: string[] = await links.allTextContents();
  
  console.log('Navigation links:', texts);
  expect(texts.length).toBeGreaterThan(0);
  
  // Filter only links containing 'Playwright'
  const playwrightLinks = texts.filter(t => t.toLowerCase().includes('playwright'));
  console.log('Playwright links:', playwrightLinks);
});

// Data-driven testing with arrays
const testData: [string, string, boolean][] = [
  ['admin@test.com', 'admin123', true],
  ['invalid@test.com', 'wrong', false],
  ['', '', false],
];

for (const [email, password, shouldPass] of testData) {
  test(`login with ${email || 'empty'} should ${shouldPass ? 'pass' : 'fail'}`, async ({ page }) => {
    await page.goto('/login');
    await page.fill('#email', email);
    await page.fill('#password', password);
    await page.click('#submit');
    
    if (shouldPass) {
      await expect(page.locator('.dashboard')).toBeVisible();
    } else {
      await expect(page.locator('.error')).toBeVisible();
    }
  });
}

❓ FAQ

πŸ€” What is the difference between string[] and Array<string>?

They are identical β€” both declare an array of strings. string[] is shorter and more commonly used. Array<string> is clearer for complex types like Array<Map<string, number>>.


πŸ€” How do I get all text from Playwright locators into an array?

Use await locator.allTextContents() β€” it returns a string[] with the text content of every matching element. For inner text, use allInnerTexts().

🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 31 TypeScript array and tuple programs with solutions →

TypeScript Functions β€” Declarations, Arrow Functions, Async & Generics with Playwright

Functions are the building blocks of clean, reusable code. In test automation, you’ll use them everywhere β€” from page actions to custom assertions to utility helpers. This guide covers every function pattern in TypeScript with real Playwright examples.

πŸ“– What You’ll Learn: Function declarations, arrow functions, optional & default parameters, rest parameters, return types, overloads, generics in functions, and async functions in Playwright.


πŸ”§ Function Declarations & Expressions

// Function declaration
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Function expression
const add = function(a: number, b: number): number {
  return a + b;
};

// Arrow function (most common in modern TypeScript)
const multiply = (a: number, b: number): number => a * b;

// Void function β€” returns nothing
function logMessage(msg: string): void {
  console.log(`[LOG]: ${msg}`);
}

console.log(greet('QA Engineer'));  // Hello, QA Engineer!
console.log(add(5, 3));            // 8
console.log(multiply(4, 7));       // 28

βš™οΈ Optional, Default & Rest Parameters

// Optional parameter (use ?)
function createUser(name: string, email?: string): string {
  return email ? `${name} (${email})` : name;
}
console.log(createUser('Deepesh'));                    // Deepesh
console.log(createUser('Deepesh', 'test@test.com'));   // Deepesh (test@test.com)

// Default parameter
function wait(ms: number = 1000): void {
  console.log(`Waiting ${ms}ms...`);
}
wait();      // Waiting 1000ms...
wait(3000);  // Waiting 3000ms...

// Rest parameters (variable number of args)
function logAll(...messages: string[]): void {
  messages.forEach((msg, i) => console.log(`${i + 1}. ${msg}`));
}
logAll('Step 1', 'Step 2', 'Step 3');

// Destructured parameters
function printConfig({ url, timeout = 30000 }: { url: string; timeout?: number }): void {
  console.log(`URL: ${url}, Timeout: ${timeout}`);
}
printConfig({ url: 'https://sqatools.in' });

πŸ“ Function Types & Callbacks

// Type alias for a function
type Validator = (value: string) => boolean;

const isEmail: Validator = (value) => value.includes('@');
const isNotEmpty: Validator = (value) => value.trim().length > 0;

console.log(isEmail('test@test.com'));  // true
console.log(isNotEmpty(''));            // false

// Callback functions
function runValidation(input: string, validator: Validator, onResult: (passed: boolean) => void): void {
  const result = validator(input);
  onResult(result);
}

runValidation('hello@test.com', isEmail, (passed) => {
  console.log(passed ? 'βœ… Valid' : '❌ Invalid');
});

// Returning functions (higher-order)
function createMultiplier(factor: number): (n: number) => number {
  return (n) => n * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

🧬 Generic Functions

// Generic function β€” works with any type
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

console.log(firstElement([1, 2, 3]));          // 1 (type: number)
console.log(firstElement(['a', 'b', 'c']));    // 'a' (type: string)

// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Deepesh', role: 'SDET' };
console.log(getProperty(user, 'name'));  // 'Deepesh'

// Generic async function
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json() as Promise<T>;
}

πŸ’‘ Generics let you write ONE function that works with strings, numbers, objects β€” anything. TypeScript figures out the type automatically when you call it.


⚑ Async Functions

// Async function returns a Promise
async function fetchTitle(url: string): Promise<string> {
  const response = await fetch(url);
  const html = await response.text();
  const match = html.match(/<title>(.*)<\/title>/);
  return match ? match[1] : 'No title found';
}

// Async arrow function
const delay = async (ms: number): Promise<void> => {
  return new Promise(resolve => setTimeout(resolve, ms));
};

// Try-catch in async functions
async function safeRequest(url: string): Promise<string | null> {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.text();
  } catch (error) {
    console.error('Request failed:', error);
    return null;
  }
}

🎭 Functions in Playwright β€” Real Examples

import { test, expect, Page } from '@playwright/test';

// Reusable login helper function
async function login(page: Page, email: string, password: string): Promise<void> {
  await page.goto('/login');
  await page.fill('#email', email);
  await page.fill('#password', password);
  await page.click('#login-btn');
  await expect(page.locator('.dashboard')).toBeVisible();
}

// Retry function for flaky operations
async function retry<T>(fn: () => Promise<T>, maxAttempts: number = 3): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts) throw error;
      console.log(`Attempt ${attempt} failed, retrying...`);
      await new Promise(r => setTimeout(r, 1000));
    }
  }
  throw new Error('Should not reach here');
}

// Using helper functions in tests
test('login and verify dashboard', async ({ page }) => {
  await login(page, 'admin@sqatools.in', 'test123');
  await expect(page).toHaveTitle(/Dashboard/);
});

test('retry flaky element click', async ({ page }) => {
  await page.goto('https://sqatools.in');
  await retry(async () => {
    await page.click('.dynamic-button');
    await expect(page.locator('.result')).toBeVisible();
  });
});

❓ FAQ

πŸ€” When should I use arrow functions vs regular functions?

Use arrow functions for callbacks, short expressions, and class methods. Use regular functions when you need hoisting or a named function in stack traces. In Playwright, arrow functions are standard for test callbacks and helpers.


πŸ€” What is the difference between optional and default parameters?

Optional parameters (name?: string) can be omitted and will be undefined. Default parameters (name: string = 'Guest') have a fallback value when omitted. Default parameters are preferred when you have a sensible default.

🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 31 TypeScript function programs with solutions →

TypeScript Loops β€” for, while, for…of & forEach with Playwright Examples

Loops let you repeat actions β€” whether it’s iterating through test data, checking multiple elements, or retrying flaky operations. This guide covers every loop type in TypeScript with real Playwright examples you’ll use daily in test automation.

πŸ“– What You’ll Learn: for loop, while loop, do…while, for…of, for…in, forEach, break/continue, and looping through Playwright elements.


πŸ” for Loop

The classic loop β€” you control the start, end, and step.

// Basic for loop
for (let i: number = 0; i < 5; i++) {
  console.log('Iteration:', i);
}
// Output: 0, 1, 2, 3, 4

// Counting backwards
for (let i: number = 10; i > 0; i -= 2) {
  console.log(i);
}
// Output: 10, 8, 6, 4, 2

// Looping through an array by index
let browsers: string[] = ['chromium', 'firefox', 'webkit'];
for (let i = 0; i < browsers.length; i++) {
  console.log(`Browser ${i + 1}: ${browsers[i]}`);
}

πŸ”„ while and do...while Loops

// while β€” checks condition BEFORE each iteration
let attempts: number = 0;
while (attempts < 3) {
  console.log(`Attempt ${attempts + 1}`);
  attempts++;
}

// do...while β€” runs at least ONCE, then checks condition
let input: string = '';
do {
  input = 'valid';  // simulating user input
  console.log('Processing input:', input);
} while (input === '');

// Retry pattern (common in test automation)
let retryCount: number = 0;
let success: boolean = false;
while (!success && retryCount < 3) {
  try {
    // attempt some operation
    success = true;
    console.log('Operation succeeded!');
  } catch {
    retryCount++;
    console.log(`Retry ${retryCount}/3`);
  }
}

⚠️ Always include a way to exit while loops (increment a counter, set a flag). An infinite loop will crash your tests!


🎯 for...of and for...in Loops

// for...of β€” iterate over VALUES (arrays, strings, maps)
let testCases: string[] = ['login', 'signup', 'checkout'];
for (const tc of testCases) {
  console.log('Running test:', tc);
}

// for...in β€” iterate over KEYS/INDICES (objects)
let config = { baseURL: 'https://sqatools.in', timeout: 30000, retries: 2 };
for (const key in config) {
  console.log(`${key}: ${config[key as keyof typeof config]}`);
}

// Iterating over a Map
let testData = new Map();
testData.set('user1', 'admin@test.com');
testData.set('user2', 'user@test.com');

for (const [key, value] of testData) {
  console.log(`${key} -> ${value}`);
}

πŸ’‘ Quick Rule: Use for...of for arrays and iterable values. Use for...in for object keys. Never use for...in on arrays β€” it iterates over indices as strings, which causes bugs.


πŸ“‹ forEach and Array Methods

let scores: number[] = [85, 92, 78, 95, 88];

// forEach β€” run a function for each element
scores.forEach((score, index) => {
  console.log(`Student ${index + 1}: ${score}`);
});

// map β€” transform each element into a new array
let grades = scores.map(s => s >= 90 ? 'A' : s >= 80 ? 'B' : 'C');
console.log(grades);  // ['B', 'A', 'C', 'A', 'B']

// filter β€” keep only elements that match
let topScores = scores.filter(s => s >= 90);
console.log(topScores);  // [92, 95]

// find β€” get the first match
let firstTop = scores.find(s => s >= 90);
console.log(firstTop);  // 92

// every/some β€” check all or any
let allPassed = scores.every(s => s >= 60);   // true
let anyPerfect = scores.some(s => s === 100); // false

⏹️ break and continue

// break β€” exit the loop entirely
for (let i = 0; i < 10; i++) {
  if (i === 5) break;
  console.log(i);
}
// Output: 0, 1, 2, 3, 4

// continue β€” skip current iteration, go to next
for (let i = 0; i < 10; i++) {
  if (i % 2 === 0) continue;  // skip even numbers
  console.log(i);
}
// Output: 1, 3, 5, 7, 9

🎭 Loops in Playwright β€” Real Examples

import { test, expect } from '@playwright/test';

test('iterate through page elements', async ({ page }) => {
  await page.goto('https://sqatools.in');
  
  // Get all navigation links and verify each one
  const navLinks = page.locator('nav a');
  const count = await navLinks.count();
  
  for (let i = 0; i < count; i++) {
    const text = await navLinks.nth(i).textContent();
    const href = await navLinks.nth(i).getAttribute('href');
    console.log(`Link ${i + 1}: ${text} -> ${href}`);
    expect(href).not.toBeNull();
  }
});

// Data-driven test with for...of
const testUsers = [
  { email: 'admin@test.com', role: 'admin' },
  { email: 'user@test.com', role: 'user' },
  { email: 'guest@test.com', role: 'guest' },
];

for (const user of testUsers) {
  test(`login as ${user.role}`, async ({ page }) => {
    await page.goto('/login');
    await page.fill('#email', user.email);
    await page.fill('#password', 'test123');
    await page.click('#login-btn');
    await expect(page.locator('.role-badge')).toHaveText(user.role);
  });
}

❓ FAQ

πŸ€” What is the difference between for...of and for...in?

for...of iterates over values (arrays, strings). for...in iterates over keys/indices (objects). Use for...of for arrays and for...in for object properties.


πŸ€” How do I loop through Playwright locators?

Use locator.count() to get the number of matches, then use a for loop with locator.nth(i) to access each element individually.

🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript loop programs with solutions →

TypeScript Conditions β€” if/else, Switch & Type Guards with Playwright Examples

Conditions let you control which code runs based on different scenarios β€” essential for building smart Playwright tests that handle popups, dynamic content, and different environments. This guide covers if/else, switch, ternary operators, and type guards with real test automation examples.

πŸ“– What You’ll Learn: if/else statements, else if chains, switch/case, ternary operator, type narrowing with conditions, and real Playwright conditional flows.


πŸ”€ if / else Statement

The most basic conditional β€” execute code only when a condition is true.

let temperature: number = 30;

if (temperature > 25) {
  console.log("It's hot outside! β˜€οΈ");
} else {
  console.log("It's cool outside! 🌀️");
}
// Output: "It's hot outside! β˜€οΈ"

πŸ”— else if Chain

Check multiple conditions in sequence β€” the first match wins.

let score: number = 85;
let grade: string;

if (score >= 90) {
  grade = "A β€” Excellent!";
} else if (score >= 80) {
  grade = "B β€” Good job!";
} else if (score >= 70) {
  grade = "C β€” Average";
} else if (score >= 60) {
  grade = "D β€” Below average";
} else {
  grade = "F β€” Needs improvement";
}

console.log(grade);  // "B β€” Good job!"

πŸ’‘ Tip: Order your conditions from most specific to least specific. TypeScript evaluates them top-to-bottom and stops at the first match.


πŸ”„ Switch Statement

When you have many possible values for a single variable, switch is cleaner than multiple if/else statements.

type Browser = 'chromium' | 'firefox' | 'webkit';
let browserName: Browser = 'chromium';

switch (browserName) {
  case 'chromium':
    console.log('🌐 Running on Chrome/Edge');
    break;
  case 'firefox':
    console.log('🦊 Running on Firefox');
    break;
  case 'webkit':
    console.log('🧭 Running on Safari');
    break;
  default:
    console.log('❓ Unknown browser');
}

// Switch with return (in a function)
function getTimeout(env: string): number {
  switch (env) {
    case 'production': return 30000;
    case 'staging':    return 15000;
    case 'local':      return 5000;
    default:           return 10000;
  }
}

⚑ Ternary Operator

A one-liner alternative to if/else β€” perfect for simple conditions.

// Syntax: condition ? valueIfTrue : valueIfFalse

let isProduction: boolean = false;
let baseURL = isProduction ? 'https://app.com' : 'https://staging.app.com';

let userAge: number = 20;
let category = userAge >= 18 ? 'Adult' : 'Minor';

// Nested ternary (use sparingly!)
let speed: number = 100;
let zone = speed > 120 ? 'Danger' : speed > 80 ? 'Fast' : 'Normal';

⚠️ Avoid nested ternaries β€” they’re hard to read. Use if/else instead when the logic is complex.


πŸ” Type Narrowing with Conditions

TypeScript gets smarter inside conditional blocks β€” it narrows the type automatically.

// typeof narrowing
function formatValue(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase();  // TypeScript knows it's a string here
  } else {
    return value.toFixed(2);     // TypeScript knows it's a number here
  }
}

// Truthiness narrowing
function greetUser(name: string | null | undefined): string {
  if (name) {
    return 'Hello, ' + name;   // name is definitely a string here
  }
  return 'Hello, Guest';
}

// in operator narrowing
interface Dog { bark(): void; breed: string; }
interface Cat { meow(): void; color: string; }

function handlePet(pet: Dog | Cat) {
  if ('bark' in pet) {
    pet.bark();    // TypeScript knows it's a Dog
  } else {
    pet.meow();    // TypeScript knows it's a Cat
  }
}

🎭 Conditions in Playwright β€” Real Examples

import { test, expect } from '@playwright/test';

test('handle dynamic page content', async ({ page }) => {
  await page.goto('https://sqatools.in');
  
  // Close popup if it appears
  const popup = page.locator('.popup-overlay');
  if (await popup.isVisible({ timeout: 3000 }).catch(() => false)) {
    await popup.locator('.close-btn').click();
    console.log('Popup closed');
  }
  
  // Environment-based navigation
  const env = process.env.TEST_ENV ?? 'staging';
  switch (env) {
    case 'production':
      await page.goto('https://sqatools.in');
      break;
    case 'staging':
      await page.goto('https://staging.sqatools.in');
      break;
    default:
      await page.goto('http://localhost:3000');
  }
  
  // Conditional assertions based on page state
  const loginBtn = page.locator('#login-btn');
  const dashboardLink = page.locator('#dashboard');
  
  if (await loginBtn.isVisible()) {
    console.log('User is not logged in');
    await loginBtn.click();
  } else if (await dashboardLink.isVisible()) {
    console.log('User is already logged in');
  }
});

❓ FAQ

πŸ€” When should I use switch vs if/else?

Use switch when comparing a single variable against multiple specific values. Use if/else when conditions are complex expressions or involve multiple variables.


πŸ€” What is type narrowing in TypeScript?

Type narrowing is when TypeScript automatically narrows a union type to a more specific type inside a conditional block. For example, after if (typeof x === 'string'), TypeScript knows x is a string.

🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 30 TypeScript conditions programs with solutions →