Practice Widgets Test modifiers · hooks · data providers
test.x · hooks · DDT · UtilElementLocator
Concept reference

test.x modifiers · hooks · data providers · UtilElementLocator

Concept page. Every snippet targets TTA-hosted pages — no third-party URLs. Covers test modifiers, test.step, serial vs parallel, hooks, parametrized data (inline / JSON / CSV / Faker), and a reusable UtilElementLocator Page-Object helper class with usage example.

① test modifiers — skip · slow · fixme · fail

Tag a test (or a whole describe) to alter how the runner treats it. Useful for known-broken specs, slow browsers, and expected-failures.

Target: /playwright/multiple_element_filter.html
tests/modifiers.spec.ts
import { test, expect } from '@playwright/test';
const URL = 'https://app.thetestingacademy.com/playwright/multiple_element_filter.html';

test('title test', async ({ page, browserName }) => {
  test.skip(browserName === 'firefox', 'Feature not yet supported on Firefox');
  await page.goto(URL);
  await expect(page).toHaveTitle(/Multiple Element Filter/, { timeout: 15000 });
});

test('email is visible (slow on firefox)', async ({ page, browserName }) => {
  test.slow(browserName === 'firefox', 'firefox is slow on this layout');
  await page.goto(URL);
  await expect(page.getByRole('textbox', { name: 'Email Address' })).toBeVisible();
});

test.fixme('password is visible — broken in Safari, fix me', async ({ page }) => {
  await page.goto(URL);
  await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible();
});

test('expected to fail until backend ships', async ({ page }) => {
  test.fail();
  await page.goto(URL);
  await expect(page.getByText('New customer area', { exact: true })).toBeVisible();
});

test.step — grouped sub-steps in the report

Wrap any await chain in test.step('label', async () => {...}). The HTML report renders each step as its own row.

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

test('login form is reachable via steps', async ({ page }) => {

  await test.step('open practice page', async () => {
    await page.goto('https://app.thetestingacademy.com/playwright/multiple_element_filter.html');
  });

  await test.step('fields are visible', async () => {
    await expect(page.getByRole('textbox', { name: 'Email Address' })).toBeVisible();
    await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible();
  });

  await test.step('submit + assert validation', async () => {
    await page.getByRole('button', { name: /Login/i }).click();
    await expect(page.getByText(/required|invalid/i)).toBeVisible();
  });
});

③ Hooks — beforeAll · beforeEach · afterEach · afterAll

Set-up / tear-down. beforeAll runs once for the file; beforeEach runs before every test. Pair with afters for cleanup.

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

test.beforeAll(async () => {
  // run once per worker — e.g. seed test data, spin a docker container
  console.log('beforeAll — server is up');
});

test.beforeEach(async ({ page }) => {
  // run before every test — e.g. log in, seed cookies
  await page.goto('https://app.thetestingacademy.com/playwright/');
});

test('practice index has 25 cards', async ({ page }) => {
  await expect(page.locator('.index-card')).toHaveCount(25);
});

test('sidebar collapse button works', async ({ page }) => {
  await page.getByLabel('Toggle sidebar').first().click();
  await expect(page.locator('.tta-shell')).toHaveAttribute('data-sidebar-collapsed', 'true');
});

test.afterEach(async ({ page }, testInfo) => {
  if (testInfo.status !== testInfo.expectedStatus) {
    await page.screenshot({ path: `out/fail-${testInfo.title}.png`, fullPage: true });
  }
});

test.afterAll(async () => {
  console.log('afterAll — tear down');
});

④ Serial vs parallel — test.describe.serial + configure({ mode })

By default Playwright runs files in parallel and tests inside a file in serial. Use describe.serial when one test depends on the prior, and configure({ mode: 'parallel' }) to opt in.

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

test.describe.serial('Checkout suite — must run in order', () => {
  test('open landing',    async () => { console.log('1'); });
  test('search product',  async () => { console.log('2'); });
  test('add to cart',     async () => { console.log('3'); });
  test('go to checkout',  async () => { console.log('4'); });
});

// These two run in parallel — independent of the serial suite above.
test('standalone A', async () => { console.log('A'); });
test('standalone B', async () => { console.log('B'); });

⑤ Data provider — inline array (parametrized tests)

Loop over an array and call test() per row. Each iteration becomes its own test with a unique title.

Target: /playwright/tables/practice.html
tests/data-inline.spec.ts
import { test, expect } from '@playwright/test';

const users = [
  { firstName: 'Aarav',  lastName: 'Sharma',  gender: 'Male'   },
  { firstName: 'Priya',  lastName: 'Nair',    gender: 'Female' },
  { firstName: 'Rohan',  lastName: 'Mehta',   gender: 'Male'   }
];

for (const user of users) {
  test(`fill profile for ${user.firstName}`, async ({ page }) => {
    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByLabel('First name').fill(user.firstName);
    await page.getByLabel('Last name').fill(user.lastName);
    await page.getByRole('radio', { name: user.gender }).check();
    await expect(page.getByLabel('First name')).toHaveValue(user.firstName);
  });
}

⑥ Data provider — JSON fixture

Pull rows from ./data/users.json committed alongside the test repo.

data/users.json
[
  { "firstName": "Aarav",  "lastName": "Sharma",  "gender": "Male" },
  { "firstName": "Priya",  "lastName": "Nair",    "gender": "Female" },
  { "firstName": "Rohan",  "lastName": "Mehta",   "gender": "Male" }
]
tests/data-json.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'fs';

type User = { firstName: string; lastName: string; gender: string };
const users: User[] = JSON.parse(fs.readFileSync('./data/users.json', 'utf-8'));

for (const user of users) {
  test(`profile · ${user.firstName}`, async ({ page }) => {
    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByLabel('First name').fill(user.firstName);
    await page.getByLabel('Last name').fill(user.lastName);
    await page.getByRole('radio', { name: user.gender }).check();
  });
}

⑦ Data provider — CSV fixture (csv-parse)

Same shape, different source. Install csv-parse first: npm i csv-parse.

data/users.csv
firstName,lastName,gender
Aarav,Sharma,Male
Priya,Nair,Female
Rohan,Mehta,Male
tests/data-csv.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'fs';
import { parse } from 'csv-parse/sync';

type User = { firstName: string; lastName: string; gender: string };

const raw = fs.readFileSync('./data/users.csv', 'utf-8');
const users: User[] = parse(raw, { columns: true, skip_empty_lines: true });

for (const user of users) {
  test(`profile · ${user.firstName}`, async ({ page }) => {
    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByLabel('First name').fill(user.firstName);
    await page.getByLabel('Last name').fill(user.lastName);
  });
}

⑧ Faker — synthetic test data

No fixture file. Generate a fresh user per test. npm i -D @faker-js/faker.

tests/data-faker.spec.ts
import { test, expect } from '@playwright/test';
import { faker } from '@faker-js/faker';

function generateUser() {
  return {
    firstName: faker.person.firstName(),
    lastName:  faker.person.lastName(),
    email:     faker.internet.email({ firstName: 'Auto' }),
    phone:     faker.phone.number({ style: 'national' }),
    password:  faker.internet.password({ length: 20, memorable: true, pattern: /[A-Z]/, prefix: 'Auto ' })
  };
}

const userCount = 5;
for (let i = 1; i <= userCount; i++) {
  test(`fresh user #${i}`, async ({ page }) => {
    const user = generateUser();
    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByLabel('First name').fill(user.firstName);
    await page.getByLabel('Last name').fill(user.lastName);
    await expect(page.getByLabel('First name')).toHaveValue(user.firstName);
  });
}

⑨ UtilElementLocator — Page-Object helper (how to create + how to use)

A thin wrapper that accepts either a string selector or a Locator object. Centralises timeouts, logging, and common actions. Drop the file at utils/UtilElementLocator.ts and import it from any test or Page Object.

How to create — utils/UtilElementLocator.ts

utils/UtilElementLocator.ts
import { Page, Locator } from '@playwright/test';

type Flex = string | Locator;

export class UtilElementLocator {
  private page: Page;
  private defaultTimeOut: number;

  constructor(page: Page, timeOut: number = 30000) {
    this.page = page;
    this.defaultTimeOut = timeOut;
  }

  private getLocator(locator: Flex): Locator {
    return typeof locator === 'string' ? this.page.locator(locator) : locator;
  }

  // ----- actions -----
  async click(locator: Flex, options?: { force?: boolean; timeout?: number }) {
    await this.getLocator(locator).click({
      force: options?.force,
      timeout: options?.timeout || this.defaultTimeOut
    });
  }
  async doubleClick(locator: Flex) { await this.getLocator(locator).dblclick({ timeout: this.defaultTimeOut }); }
  async rightClick(locator: Flex)  { await this.getLocator(locator).click({ button: 'right', timeout: this.defaultTimeOut }); }
  async fill(locator: Flex, text: string) { await this.getLocator(locator).fill(text, { timeout: this.defaultTimeOut }); }
  async type(locator: Flex, text: string, delay = 500) {
    await this.getLocator(locator).pressSequentially(text, { delay, timeout: this.defaultTimeOut });
  }
  async clear(locator: Flex) { await this.getLocator(locator).clear({ timeout: this.defaultTimeOut }); }

  // ----- reads -----
  async getText(locator: Flex)       { return await this.getLocator(locator).textContent(); }
  async getInnerText(locator: Flex)  { return (await this.getLocator(locator).innerText()).trim(); }
  async getAttr(locator: Flex, name: string) { return await this.getLocator(locator).getAttribute(name); }
  async getValue(locator: Flex)      { return await this.getLocator(locator).inputValue(); }
  async getAllTexts(locator: Flex)   { return await this.getLocator(locator).allInnerTexts(); }

  // ----- state checks -----
  async isVisible(locator: Flex)   { return await this.getLocator(locator).isVisible(); }
  async isEnabled(locator: Flex)   { return await this.getLocator(locator).isEnabled(); }
  async isChecked(locator: Flex)   { return await this.getLocator(locator).isChecked(); }
  async isEditable(locator: Flex)  { return await this.getLocator(locator).isEditable(); }

  // ----- waits -----
  async waitForVisible(locator: Flex, timeout = 5000) {
    try { await this.getLocator(locator).waitFor({ state: 'visible', timeout }); return true; }
    catch { return false; }
  }
  async waitForPageLoad(state: 'load' | 'domcontentloaded' | 'networkidle' = 'load') {
    await this.page.waitForLoadState(state);
  }

  // ----- selects -----
  async selectByText(locator: Flex, text: string) {
    await this.getLocator(locator).selectOption({ label: text }, { timeout: this.defaultTimeOut });
  }
  async selectByValue(locator: Flex, value: string) {
    await this.getLocator(locator).selectOption({ value }, { timeout: this.defaultTimeOut });
  }
  async selectByIndex(locator: Flex, index: number) {
    await this.getLocator(locator).selectOption({ index }, { timeout: this.defaultTimeOut });
  }
}

How to use — tests/profile.spec.ts

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

test('fill profile via UtilElementLocator', async ({ page }) => {
  const ui = new UtilElementLocator(page, 15000);

  await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
  await ui.waitForPageLoad('networkidle');

  // strings for selectors, Locator objects also accepted
  await ui.fill('#first-name', 'Aarav');
  await ui.fill('#last-name', 'Sharma');

  await ui.click(page.getByRole('radio', { name: 'Male' }));
  await ui.selectByValue('#years-experience', '5');

  const firstNameValue = await ui.getValue('#first-name');
  expect(firstNameValue).toBe('Aarav');

  await ui.click('[data-testid="profile-submit"]');
});