Concept reference - Data-driven + Page Objects

Data-driven testing + Page Object Model

Eleven patterns covering Faker, inline arrays, JSON, CSV, XLSX, hooks, and a clean Page Object Model with a shared UtilElementLocator helper. Every snippet targets TTA practice pages (https://app.thetestingacademy.com/playwright/multiple_element_filter.html for login, https://app.thetestingacademy.com/playwright/tables/practice.html for register/QA-profile). No third-party URLs.

(1) Faker - single user registration

One test, one fresh user generated at call time. Install: npm i -D @faker-js/faker.

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

test(`Register single user with Faker`, async ({ page }) => {
  const firstName = faker.person.firstName();
  const lastName  = faker.person.lastName();
  const email     = faker.internet.email({ firstName: 'Auto' });
  const telephone = faker.phone.number({ style: 'national' });
  const password  = faker.internet.password({ length: 20, memorable: true, pattern: /[A-Z]/, prefix: 'Auto ' });

  await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
  await page.getByRole('textbox', { name: 'First Name' }).fill(firstName);
  await page.getByRole('textbox', { name: 'Last Name' }).fill(lastName);
  await page.getByRole('textbox', { name: 'Email' }).fill(email);
  await page.getByRole('textbox', { name: 'Phone' }).fill(telephone);
  await page.getByRole('textbox', { name: 'Password' }).first().fill(password);

  await page.getByRole('button', { name: 'Save profile' }).click();
  await expect(page.locator('#submission-output')).toContainText(firstName);
});

(2) Faker - generic generateUser() utility

Factor the fake-data generation out so any test can grab a fresh user with one call.

tests/register-faker-util.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' }),
    telephone: faker.phone.number({ style: 'national' }),
    password:  faker.internet.password({ length: 20, memorable: true, pattern: /[A-Z]/, prefix: 'Auto ' }),
  };
}

test(`Register single user via generateUser()`, async ({ page }) => {
  const user = generateUser();

  await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
  await page.getByRole('textbox', { name: 'First Name' }).fill(user.firstName);
  await page.getByRole('textbox', { name: 'Last Name' }).fill(user.lastName);
  await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
  await page.getByRole('textbox', { name: 'Password' }).first().fill(user.password);
  await page.getByRole('button', { name: 'Save profile' }).click();
  await expect(page.locator('#submission-output')).toContainText(user.firstName);
});

(3) Faker - parametrised loop (N tests)

Generate N independent tests with Faker data. Each iteration becomes its own Playwright test, so failures isolate cleanly and run in parallel.

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

const userCount = 8;

for (let i = 1; i <= userCount; i++) {
  test(`Register new user# ${i}`, async ({ page }) => {
    const user = {
      firstName: faker.person.firstName(),
      lastName:  faker.person.lastName(),
      email:     faker.internet.email({ firstName: 'Auto' }),
      telephone: faker.phone.number({ style: 'national' }),
      password:  faker.internet.password({ length: 20, memorable: true, pattern: /[A-Z]/, prefix: 'Auto ' }),
    };

    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByRole('textbox', { name: 'First Name' }).fill(user.firstName);
    await page.getByRole('textbox', { name: 'Last Name' }).fill(user.lastName);
    await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
    await page.getByRole('textbox', { name: 'Password' }).first().fill(user.password);
    await page.getByRole('button', { name: 'Save profile' }).click();
    await expect(page.locator('#submission-output')).toContainText(user.firstName);
  });
}

(4) Faker + predefined email domains

Sometimes you need real-looking emails. Faker the name, then craft the email from a curated domain list. One iteration per domain.

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

const totalUserCount = 5;
const emailDomains  = ['gmail.com', 'yahoo.com', 'outlook.com', 'tta.dev', 'icloud.com'];

for (let i = 1; i <= totalUserCount; i++) {
  test(`Register user# ${i} (${emailDomains[i-1]})`, async ({ page }) => {
    const firstName = faker.person.firstName();
    const lastName  = faker.person.lastName();
    const email     = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${emailDomains[i-1]}`;
    const password  = faker.internet.password({ length: 20, memorable: true, pattern: /[A-Z]/, prefix: 'Auto ' });

    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByRole('textbox', { name: 'First Name' }).fill(firstName);
    await page.getByRole('textbox', { name: 'Last Name' }).fill(lastName);
    await page.getByRole('textbox', { name: 'Email' }).fill(email);
    await page.getByRole('textbox', { name: 'Password' }).first().fill(password);
    await page.getByRole('button', { name: 'Save profile' }).click();
    await expect(page.locator('#submission-output')).toContainText(email);
  });
}

(5) Inline array - swap to login test

Smallest DDT footprint. Hard-coded array, one test per row. Switch the SUT here to the login page to keep it varied.

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

const loginData = [
  { username: '[email protected]', password: 'test123' },
  { username: '[email protected]',  password: 'test123' },
];

for (const data of loginData) {
  test(`login test for ${data.username}`, async ({ page }) => {
    await page.goto('https://app.thetestingacademy.com/playwright/multiple_element_filter.html');
    await page.getByRole('textbox', { name: 'Username' }).fill(data.username);
    await page.getByRole('textbox', { name: 'Password' }).fill(data.password);
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page).toHaveURL(/multiple_element_filter/);
  });
}

(6) JSON fixture file

Read once at module load, parse with the built-in JSON.parse. Use a TS type so each row is statically typed. Download register.json above.

tests/register-json.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'fs';

type RegData = {
  firstName: string;
  lastName:  string;
  telephone: string;
  password:  string;
  subscribeNewsletter: string;
};

const registerData: RegData[] = JSON.parse(
  fs.readFileSync('./data/register.json', 'utf-8')
);

for (const user of registerData) {
  test(`Register from JSON: ${user.firstName}`, async ({ page }) => {
    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByRole('textbox', { name: 'First Name' }).fill(user.firstName);
    await page.getByRole('textbox', { name: 'Last Name' }).fill(user.lastName);
    await page.getByRole('textbox', { name: 'Email' }).fill(getRandomEmail());
    await page.getByRole('textbox', { name: 'Phone' }).fill(user.telephone);
    await page.getByRole('textbox', { name: 'Password' }).first().fill(user.password);

    if (user.subscribeNewsletter === 'Yes') {
      await page.getByRole('radio', { name: 'Yes' }).click();
    } else {
      await page.getByRole('radio', { name: 'No' }).click();
    }
    await page.getByRole('button', { name: 'Save profile' }).click();
    await expect(page.locator('#submission-output')).toContainText(user.firstName);
  });
}

function getRandomEmail(): string {
  const randomValue = Math.random().toString(36).substring(2, 9);
  return `auto_${randomValue}@tta.dev`;
}

(7) CSV fixture - csv-parse/sync

Install: npm i -D csv-parse. Read the file synchronously at module load, parse with columns: true so each row becomes an object keyed by header. Download register.csv above.

tests/register-csv.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'fs';
import { parse } from 'csv-parse/sync';

type RegData = {
  firstName: string; lastName: string;
  telephone: string; password: string;
  subscribeNewsletter: string;
};

const fileContent = fs.readFileSync('./data/register.csv', 'utf-8');
const registerData: RegData[] = parse(fileContent, {
  columns: true,
  skip_empty_lines: true,
});

for (const user of registerData) {
  test(`Register from CSV: ${user.firstName}`, async ({ page }) => {
    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByRole('textbox', { name: 'First Name' }).fill(user.firstName);
    await page.getByRole('textbox', { name: 'Last Name' }).fill(user.lastName);
    await page.getByRole('textbox', { name: 'Email' }).fill(getRandomEmail());
    await page.getByRole('textbox', { name: 'Phone' }).fill(user.telephone);
    await page.getByRole('textbox', { name: 'Password' }).first().fill(user.password);

    if (user.subscribeNewsletter === 'Yes') {
      await page.getByRole('radio', { name: 'Yes' }).click();
    } else {
      await page.getByRole('radio', { name: 'No' }).click();
    }
    await page.getByRole('button', { name: 'Save profile' }).click();
    await expect(page.locator('#submission-output')).toContainText(user.firstName);
  });
}

function getRandomEmail(): string {
  const randomValue = Math.random().toString(36).substring(2, 9);
  return `auto_${randomValue}@tta.dev`;
}

(8) Excel (XLSX) fixture - xlsx

Install: npm i -D xlsx. Read workbook, pick a named sheet, convert to JSON. Hit the users.xlsx button above to download a generated workbook with the same shape.

tests/register-xlsx.spec.ts
import { test, expect } from '@playwright/test';
import XLSX from 'xlsx';

const workbook = XLSX.readFile('./data/users.xlsx');
const sheet    = workbook.Sheets['register'];
const users    = XLSX.utils.sheet_to_json(sheet);   // excel -> JSON

for (const [index, user] of users.entries()) {
  const { firstName, lastName, phone, password } = user as any;

  test(`Register from XLSX #${index+1}: ${firstName}`, async ({ page }) => {
    await page.goto('https://app.thetestingacademy.com/playwright/tables/practice.html');
    await page.getByRole('textbox', { name: 'First Name' }).fill(firstName);
    await page.getByRole('textbox', { name: 'Last Name' }).fill(lastName);
    await page.getByRole('textbox', { name: 'Email' }).fill(getRandomEmail());
    await page.getByRole('textbox', { name: 'Phone' }).fill(phone);
    await page.getByRole('textbox', { name: 'Password' }).first().fill(password);
    await page.getByRole('button', { name: 'Save profile' }).click();
    await expect(page.locator('#submission-output')).toContainText(firstName);
  });
}

function getRandomEmail(): string {
  const randomValue = Math.random().toString(36).substring(2, 9);
  return `auto_${randomValue}@tta.dev`;
}

(9) Hooks - beforeAll / beforeEach / afterEach / afterAll

Order: beforeAll -> (beforeEach -> test body -> afterEach) per test -> afterAll. Use them for shared setup (server up, logged-in session) and cleanup (logout, close browser).

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

test.beforeAll(async () => {
  console.log('beforeAll  -- server is up and running!');
});

test.beforeEach(async () => {
  console.log('beforeEach -- user is logged in!');
});

test('Home Page Test', async () => {
  console.log('home page test');
});

test('Search Product Test', async () => {
  console.log('search product test');
});

test('Cart Test', async () => {
  console.log('cart test');
});

test.afterEach(async () => {
  console.log('afterEach  -- logout!');
});

test.afterAll(async () => {
  console.log('afterAll   -- close browser!');
});

(10) POM + DDT combined

Read CSV, hand each row to the page-object. Test stays declarative, no page. calls leak out of the POM.

tests/register-pom-ddt.spec.ts
import { expect, test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { RegisterPage } from '../pages/RegisterPage';
import fs from 'fs';
import { parse } from 'csv-parse/sync';

type RegData = {
  firstName: string; lastName: string;
  telephone: string; password: string;
  subscribeNewsletter: string;
};

const fileContent = fs.readFileSync('./data/register.csv', 'utf-8');
const registerData: RegData[] = parse(fileContent, {
  columns: true, skip_empty_lines: true,
});

for (const user of registerData) {
  test(`@register verify user is able to register ${user.firstName}`, async ({ page, baseURL }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goToLoginPage(baseURL);
    const registerPage: RegisterPage = await loginPage.navigateToRegisterPage();
    const isUserRegistered: boolean = await registerPage.registerUser(
      user.firstName,
      user.lastName,
      getRandomEmail(),
      user.telephone,
      user.password,
      user.subscribeNewsletter,
    );
    expect(isUserRegistered).toBeTruthy();
  });
}

function getRandomEmail(): string {
  const randomValue = Math.random().toString(36).substring(2, 9);
  return `auto_${randomValue}@tta.dev`;
}

(11) Page Object Model classes (with UtilElementLocator)

Each page-object exposes locators (private) and actions (public). All low-level interactions go through a shared UtilElementLocator helper so timeouts, logging and waits live in one place.

pages/LoginPage.ts

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

export class LoginPage {
  // 1. page locators / object repository
  private readonly page: Page;
  private readonly eleUtil: UtilElementLocator;
  private readonly emailId: Locator;
  private readonly password: Locator;
  private readonly loginBtn: string;
  private readonly warningMsg: Locator;
  private readonly registerLink: Locator;

  // 2. constructor
  constructor(page: Page) {
    this.page         = page;
    this.eleUtil      = new UtilElementLocator(page);
    this.emailId      = page.getByRole('textbox', { name: 'Username' });
    this.password     = page.getByRole('textbox', { name: 'Password' });
    this.loginBtn     = 'button[type="submit"]';
    this.warningMsg   = page.locator('.alert.alert-danger');
    this.registerLink = page.getByText('Register', { exact: true });
  }

  // 3. page actions / methods
  async goToLoginPage(baseURL: string | undefined) {
    await this.page.goto(baseURL + '/playwright/multiple_element_filter.html');
  }

  async doLogin(email: string, password: string): Promise<HomePage> {
    await this.eleUtil.fill(this.emailId, email);
    await this.eleUtil.fill(this.password, password);
    await this.eleUtil.click(this.loginBtn, { force: true, timeout: 5000 });
    return new HomePage(this.page);
  }

  async getInvalidLoginMessage(): Promise<string | null> {
    const errorMsg = await this.eleUtil.getText(this.warningMsg);
    console.log('invalid login warning: ' + errorMsg);
    return errorMsg;
  }

  async navigateToRegisterPage(): Promise<RegisterPage> {
    await this.eleUtil.click(this.registerLink, { force: true }, 1);
    return new RegisterPage(this.page);
  }
}

pages/HomePage.ts

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

export class HomePage {
  readonly page: Page;
  private readonly eleUtil: UtilElementLocator;
  private readonly loginLink: Locator;
  private readonly logoutLink: Locator;
  private readonly search: Locator;
  private readonly searchIcon: Locator;

  constructor(page: Page) {
    this.page       = page;
    this.eleUtil    = new UtilElementLocator(page);
    this.logoutLink = page.getByRole('link', { name: 'Logout' });
    this.loginLink  = page.getByRole('link', { name: 'Login' });
    this.search     = page.getByRole('textbox', { name: 'Search' });
    this.searchIcon = page.locator('#search button');
  }

  async isUserLoggedIn(): Promise<boolean> {
    return await this.eleUtil.isVisible(this.logoutLink, 0);
  }

  async logout(): Promise<LoginPage> {
    await this.eleUtil.click(this.logoutLink, { timeout: 5000 }, 1);
    await this.eleUtil.click(this.loginLink,  { timeout: 5000 }, 1);
    return new LoginPage(this.page);
  }

  async doSearch(searchKey: string): Promise<ResultsPage> {
    console.log(`search key: ${searchKey}`);
    await this.eleUtil.fill(this.search, searchKey);
    await this.eleUtil.click(this.searchIcon);
    return new ResultsPage(this.page);
  }
}

pages/RegisterPage.ts

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

export class RegisterPage {
  private readonly page: Page;
  private readonly eleUtil: UtilElementLocator;
  private readonly firstNameInput: Locator;
  private readonly lastNameInput: Locator;
  private readonly emailInput: Locator;
  private readonly telephoneInput: Locator;
  private readonly passwordInput: Locator;
  private readonly confirmPasswordInput: Locator;
  private readonly newsletterYesRadio: Locator;
  private readonly newsletterNoRadio: Locator;
  private readonly agreeCheckbox: Locator;
  private readonly continueButton: Locator;
  private readonly successMsg: Locator;

  constructor(page: Page) {
    this.page    = page;
    this.eleUtil = new UtilElementLocator(page);

    this.firstNameInput       = page.getByRole('textbox', { name: 'First Name' });
    this.lastNameInput        = page.getByRole('textbox', { name: 'Last Name' });
    this.emailInput           = page.getByRole('textbox', { name: 'Email' });
    this.telephoneInput       = page.getByRole('textbox', { name: 'Phone' });
    this.passwordInput        = page.getByRole('textbox', { name: 'Password' }).first();
    this.confirmPasswordInput = page.getByRole('textbox', { name: 'Password Confirm' });
    this.newsletterYesRadio   = page.getByRole('radio', { name: 'Yes' });
    this.newsletterNoRadio    = page.getByRole('radio', { name: 'No' });
    this.agreeCheckbox        = page.locator('[name="agree"]');
    this.continueButton       = page.getByRole('button', { name: 'Save profile' });
    this.successMsg           = page.locator('#submission-output');
  }

  async registerUser(
    firstName: string,
    lastName:  string,
    email:     string,
    telephone: string,
    password:  string,
    subscribeNewsletter: string,
  ): Promise<boolean> {
    await this.eleUtil.fill(this.firstNameInput, firstName);
    await this.eleUtil.fill(this.lastNameInput, lastName);
    await this.eleUtil.fill(this.emailInput, email);
    await this.eleUtil.fill(this.telephoneInput, telephone);
    await this.eleUtil.fill(this.passwordInput, password);
    await this.eleUtil.fill(this.confirmPasswordInput, password);

    if (subscribeNewsletter === 'Yes') {
      await this.eleUtil.click(this.newsletterYesRadio);
    } else {
      await this.eleUtil.click(this.newsletterNoRadio);
    }

    await this.eleUtil.click(this.agreeCheckbox);
    await this.eleUtil.click(this.continueButton);

    await expect(this.successMsg).toContainText(firstName);
    return true;
  }
}

pages/ProductInfoPage.ts

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

export class ProductInfoPage {
  private readonly page: Page;
  private readonly eleUtil: UtilElementLocator;
  private readonly header: Locator;
  private readonly imageCount: Locator;
  private readonly productMetaData: Locator;
  private readonly productPriceData: Locator;
  private readonly productMap = new Map<string, string | number | null>();

  constructor(page: Page) {
    this.page             = page;
    this.eleUtil          = new UtilElementLocator(page);
    this.header           = page.locator('h1');
    this.imageCount       = page.locator('div#content img');
    this.productMetaData  = page.locator("(//div[@id='content']//ul[@class='list-unstyled'])[1]/li");
    this.productPriceData = page.locator("(//div[@id='content']//ul[@class='list-unstyled'])[2]/li");
  }

  async getProductHeader(): Promise<string> {
    const header = await this.eleUtil.getInnerText(this.header);
    return header.trim();
  }

  async getProductDetails(): Promise<Map<string, string | number | null>> {
    this.productMap.set('header', await this.getProductHeader());
    this.productMap.set('imagecount', await this.imageCount.count());
    await this.getProductMetaData();
    await this.getProductPricingData();
    return this.productMap;
  }

  private async getProductMetaData() {
    const rows = await this.productMetaData.allInnerTexts();
    for (const meta of rows) {
      const [k, v] = meta.split(':');
      this.productMap.set(k.trim(), v.trim());
    }
  }

  private async getProductPricingData() {
    const rows = await this.productPriceData.allInnerTexts();
    this.productMap.set('price', rows[0].trim());
    this.productMap.set('extaxprice', rows[1].split(':')[1].trim());
  }
}

pages/ResultsPage.ts

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

export class ResultsPage {
  private readonly page: Page;
  private readonly eleUtil: UtilElementLocator;
  private readonly results: Locator;

  constructor(page: Page) {
    this.page    = page;
    this.eleUtil = new UtilElementLocator(page);
    this.results = page.locator('.product-thumb');
  }

  async getSearchResultsCount(): Promise<number> {
    await this.page.waitForTimeout(2000);
    return await this.results.count();
  }

  async selectProduct(productName: string): Promise<ProductInfoPage> {
    await this.eleUtil.click(this.page.getByRole('link', { name: productName }));
    return new ProductInfoPage(this.page);
  }
}

For the UtilElementLocator source (string-or-Locator Flex type, click / fill / type / getText / waitFor / select methods), see the Test modifiers page.