The Testing Academy · Class Notes Friday, 7 August (IST)
Live class · study guide

Dynamic data with Faker, and the page object model

Faker JS for test data that is different on every run, then the page object model: one class per screen, locators and actions inside, assertions left in the spec. Plus the .or() fallback chain and the Snap Locator extension that generates the class for you.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright class, rebuilt from the session recording, with code cross-checked against the batch notes deck. Every Faker call and every type on this page was executed against @faker-js/faker 10.6.0 and type-checked against @playwright/test 1.61.1. Selector strings specific to the practice app are left as placeholders.

01

Two problems, one class

The previous session put test data in files, but the data was still static: the same five rows every run. This class fixed that with Faker, then moved to the last structural topic before the advanced framework begins: the page object model.

02

Faker JS: data that is different every time

Faker is a third-party JavaScript library, not a Playwright package. Playwright ships test and expect. Everything else comes from npm.

Terminal
npm install --save @faker-js/faker
TypeScript
import { test, expect } from '@playwright/test';
import { faker } from '@faker-js/faker';

The case for realistic data over made-up data came from a real defect. On a production application, a user's legal first name contained a digit, part of a generational suffix. Nobody had written a test for a digit in a first name because nobody imagined one. Faker generates from real-world name distributions, so it produces the long names, the apostrophes and the accents that a developer typing test1 never will.

The API is organised by category:

TypeScript
const firstName = faker.person.firstName();      // 'Felicia'
const lastName  = faker.person.lastName();       // 'McClure'
const email     = faker.internet.email();        // 'Carmen_Nader@yahoo.com'
const password  = faker.internet.password();     // 'LgVpvKofXCTuJkL'
const phone     = faker.phone.number({ style: 'national' });  // '(823) 415-7983'

Email lives under internet, not person, and phone numbers live at the top level under phone. Both were tried the other way round in class and corrected on the spot. faker.person.email() and faker.internet.phone.number() are both TypeError. When you are unsure, type faker. and let autocomplete answer, which is exactly what happened live.

Storing values you will assert on later

Data that changes every run is useless if you never captured what it was. When an assertion later needs the value, put it in a variable first:

TypeScript
const expectedFirstName = faker.person.firstName();

await page.getByRole('textbox', { name: 'First Name' }).fill(expectedFirstName);
await page.getByRole('button', { name: 'Save profile' }).click();

await expect(page.locator('#profile-name')).toHaveText(expectedFirstName);

Calling faker.person.firstName() a second time inside the assertion would generate a different name and fail. This is the single most common way a Faker test goes wrong.

The rule about user input

A question about letting the user set the generation rules got a firm answer, worth recording because it is a framework principle rather than a Faker detail:

Automation frameworks do not take user input.

Inputs arrive from Excel files, config files, or JSON. Nothing prompts. A suite of twelve or thirteen thousand tests cannot pause for a human, and a test that behaves differently depending on what someone typed is not a test.

What Faker cannot do

It generates values, not artefacts. There is no call that produces a PNG, an XLSX or a PDF for an upload test. The feature list on the library's own site is the boundary: person, location, date, finance, commerce, hacker, number and string, plus localisation across 70-odd locales. File fixtures stay checked into the repository.

A user factory

Rather than scattering faker. calls through the spec, the class wrapped them:

TypeScript
function generateUser() {
  return {
    firstName: faker.person.firstName(),
    lastName:  faker.person.lastName(),
    email:     faker.internet.email(),
    phone:     faker.phone.number({ style: 'international' }),
    password:  faker.internet.password(),
  };
}

One call, a whole user, and one place to change when the application adds a field.

Faker plus data-driven testing

Combining the loop from the previous class with generated data produces five registrations across five email domains:

TypeScript
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);
  });
}

The email is built rather than generated, so the assertion has something predictable to check while the name behind it still varies. ${emailDomains[i - 1]} in the title keeps all five test names distinct, which Playwright requires.

That password call has a trap in it, and it is worth knowing before you copy the line into a form with a complexity rule. memorable: true silently ignores pattern. Generating 200 passwords with { memorable: true, pattern: /[A-Z]/ } produced an uppercase letter 0 times; the identical call with memorable: false produced one 200 times. Memorable mode builds pronounceable lowercase syllables and the pattern never gets a look in. If the field under test demands a capital, drop memorable. Note also that length: 20 is the total including prefix, not 20 characters after it.

03

Why the page object model exists

The honest version of the argument is about scale. Keeping locators inside the spec is fine at 100 or 200 tests. The class polled the room and the answers ran from 150 to 15,000 tests, and past a couple of hundred the approach stops working: one changed button means editing every file that touched it.

Every Playwright test has the same three parts, the AAA shape:

  • Arrange, the fixtures. async ({ page }) gives you browser, context and page.
  • Act, the locators and the interactions.
  • Assert, the expectations.

The page object model moves arrange and act into classes and leaves assert in the spec.

Without POM login.spec.ts Arrangepage fixture, goto Actevery locator, inline every fill and click Assert one changed button = edit every spec With POM pages/login-page.ts Page locators readonly, set once in the constructor Page actions goto(), login() async methods login.spec.ts Assert new LoginPage(page) loginPage.login(...) expect(...) no locators no waits no selectors one changed button = edit one class
The test stops knowing how the login form works. That is the entire benefit.

The definition, as given:

The page object model is a design pattern where you create a separate class for each page of your application. The class contains all the locators and methods for that page. Your test files never touch locators directly, they only call page object methods.

Walking the practice shop gave the page list: login, inventory, cart, checkout one, checkout two, order review, order confirmation. Seven screens, seven classes, each with exactly two things in it: page locators and page actions.

04

Writing the class

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

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.locator('#email');
    this.passwordInput = page.locator('#password');
    this.loginButton = page.getByTestId('login-button');
  }

  async goto() {
    await this.page.goto('https://app.thetestingacademy.com/practice/login');
  }

  async login(username: string, password: string) {
    await this.emailInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}

Five things in that file are deliberate, and each was a question in the room:

  • It is a .ts file, not .spec.ts. The .spec.ts suffix is reserved for test files. Name a page object .spec.ts and Playwright will try to run it as a test and find none.
  • readonly means assign once, in the constructor, then never reassign. It is a TypeScript-only marker, erased at compile time, and it encodes the intent that a locator is fixed for the life of the object.
  • The constructor takes page. That is the binding to the browser context this object works against. No page, no locators.
  • export is mandatory. Without it the class is invisible to every other file.
  • Locators are lazy. page.locator('#email') does not touch the DOM. Nothing is queried until an action or an assertion runs, which is why the constructor can build locators for elements that do not exist yet.

The spec is then almost empty, which is the point:

TypeScript
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';

test('user can log in', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('admin', 'password');

  await expect(page).toHaveTitle('TTA Login');
});

Every page object follows the identical three-step recipe: declare the locators, initialise them in the constructor, write the action methods. Inventory, cart and checkout are the same file with different nouns.

Page objects are classes, not interfaces. An interface describes a shape and disappears at runtime; you cannot new it and it cannot hold behaviour. Also, importing expect into a page object is legal but usually a smell: assertions belong in the spec, otherwise the class starts deciding what "correct" means and the test can no longer say.

05

Locator chaining with .or()

A locator can carry its own fallbacks:

TypeScript
this.loginButton = page
  .getByRole('button', { name: 'Login to Practice Account' })
  .or(page.getByTestId('login-button'))
  .or(page.getByText('Login to Practice Account'));

The Snap Locator extension offers four tiers for any element (recommended, smart, alternate, fallback) which map naturally onto a chain like this one.

.click() one action getByRole('button', { name: ... }) getByTestId('login-button') getByText('Login to ...') matched first first match wins one timeout, not three All three are evaluated together. Nothing is tried, timed out, and then retried.
Alternatives, not a sequence. The timeouts do not stack.

The resolution rule is the part people get wrong. Playwright does not try the first, wait for it to time out, then try the second. It waits until any one of the alternatives matches, and the first match wins, under a single timeout.

A question from the room assumed a chain would push the failure rate up. It does the opposite: a locator with three ways to match is harder to break than a locator with one, so a cosmetic markup change stops failing the suite.

Calling this self-healing is generous, and it was flagged as such. Nothing learns and nothing rewrites itself. It is a set of alternatives written by you in advance. Real self-healing implies a tool that repairs the selector after a failure, which this is not. Note also that .or() is Playwright-specific: Selenium has no equivalent.

Auto-waiting is already built in, chain or no chain. Playwright waits for the element to be attached, visible, stable and enabled before acting. There is no explicit wait and no implicit wait to configure.

06

Generating page objects instead of typing them

The class demonstrated Snap Locator, a free open-source Chrome extension, MIT-licensed, built for this course. It is installed by downloading it and dragging it into the browser's extensions page.

The flow: pick an element, press plus, pick the next, press plus, then generate. Out comes a page object class with locators already named, ready to paste. It supports Playwright and Selenium, offers the four locator tiers, and keeps a history of what you picked.

On the two questions that came up:

  • Is it safe for a locked-down organisation? It is open source and captures nothing: no tracking, no ads, no data collection. The source is on GitHub, so the review is a code review. Several commercial locator extensions do harvest data, which is the reason to prefer an auditable one.
  • What if extensions are banned outright? Download the source, hand it to your in-house AI assistant, and have it generate the equivalent helper internally.

Generated code needs a pass by hand. The extension picks up extra elements you did not want, and image-based locators in particular come out brittle. Delete what you do not need.

The extension was written by prompting an AI model rather than hand-coded, which was stated openly in class along with the honest consequence: the author cannot answer questions about the internals line by line. Worth carrying as a general point, not a criticism. Generated code you have not read is code you cannot support, and for a throwaway utility that is a fine trade, while for something in your test path it is a decision to make deliberately.

07

Tasks and announcements

  • Practise the previous session's data-driven work if you have not: JSON, CSV, Excel, YAML and MySQL sources.
  • Build one page object by hand before using the generator. The generator is faster, but the interview asks you to write the class.
  • Watch the recorded sessions on Playwright CLI, Playwright MCP and Playwright AI Agents before the advanced framework starts. Separate tasks are being added for each.
  • No Sunday class. Use the recordings.
  • Coming next, the advanced framework: fixtures, page objects, calendar handling, regex patterns, window and tab handling, Cucumber, API testing, an AI agent factory and an LLM gateway.
  • Do not miss the next few sessions. They build one framework end to end, and each one assumes the last.

Interview forms of today's material: why is a page object a .ts file and not a .spec.ts? What does readonly buy you when the locator was never going to be reassigned anyway? Explain why .or() does not stack timeouts. Why must a Faker value be stored in a variable before you assert on it?