|

Day 15: Playwright Assertions, Hooks, and Data-Driven Tests (CSV, JSON, Faker)

Compact diagram of Playwright hooks: beforeEach, test, afterEach

This is Day 15 of the 21-day JS to Playwright Framework series. One lesson a day. JavaScript first. TypeScript next. Playwright Test now. Framework later.

Days 1–7 were the language. Day 8 was the object model. Day 9 typed that object. Day 10 installed Playwright and handed you { page }. Day 11 found a field. Day 12 saved a session and read a table. Day 13 chose a document. Day 14 opened SVG, Shadow DOM, upload, download, and scroll. Today the suite has to *prove* something, *prepare* something, and *multiply* something.

I am Pramod Dutta. I teach SDETs in India for a living. The week I open assertions, someone always writes await page.click('#login') and calls it a test. The week I open hooks, someone pastes the same goto into eight files. The week I open data-driven tests, someone copies the login spec five times and changes the email by hand. That is not a suite. That is a folder of demos.

This is not the existing 21-Day Playwright with TypeScript Challenge. That series starts later in the stack. This series started at console.log. Day 15 is the first day the spec has to choose how it fails, when it sets up, and how many times it runs the same flow.

All labs come from my public fundamentals repo: LearningPlaywrightFundamentals on branch main. I fetched three folders from raw GitHub: tests/17_Expect_Assertions, tests/18_Test_hooks, and tests/19_Data_Driven_Testing. I quote those files. I will not invent a file that is not there.

Classroom spellings stay. The URL spec is 257_URL_Asserations.spec.ts — Asserations, as GitHub serves it. The annotations file is 258_Test_HOOK.spec.ts — HOOK, singular. Lab 260 titles a test practice index has 25 cards and then asserts .index-card toHaveCount(29). Lab 265 is named 265_DDT_JSON.spec.ts, imports registration-data.json, and still titles the describe DDT CSV. I do not rename files to make this post prettier.

yamlReader.ts and xlsxReader.ts exist in folder 19. No spec on main imports them. I will show the helpers. I will not invent a 266_DDT_YAML.spec.ts.

Custom fixtures — test.extend — are not in these three folders. They sit in tests/21_Fixture/272_Fixture_Placeholder.spec.ts, a skipped placeholder. The diagram at the top of this post is the built-in { page } fixture versus a hook versus a DDT loop. Do not read “fixture” here as a custom worker-scoped login. That is Day 16, and even then the file is a skip.

If you want the video plus project path after you finish these 21 posts, the course is here: Playwright Automation Mastery. The series hub for every day lives here: JavaScript to TypeScript to Playwright Advanced Framework 21-Day Guide.

*Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.*

Compact diagram of Playwright hooks: beforeEach, test, afterEach

Contents

What you will be able to do after Day 15

By the end of this post you can:

  1. Tell a synchronous value assertion (expect(1 + 2).toBe(3)) from an auto-retrying locator assertion (await expect(heading).toBeVisible()).
  2. Use toEqual for objects and arrays, and admit that toBe is ===.
  3. Soft-assert a First name field and still run a hard toBeEnabled after the soft block, the way 256_Expect.spec.ts does.
  4. Negate with .not on a locator (#error not visible) and on a value (title not containing error).
  5. Assert title and URL on the calendar widget, then toBeChecked / toBeEnabled / toBeVisible on the practice-tables page — from 257_URL_Asserations.spec.ts.
  6. Read Expect_Assertions_Cheatsheet.md as the interview one-pager, and treat More_Expect_Examples.md as the long reference — without pretending playwright-assertions.spec.ts exists.
  7. Annotate with test.skip, test.slow, test.fixme, and test.fail the way 258_Test_HOOK.spec.ts does, including the Firefox conditions.
  8. Split one login-form scenario into named test.step phases so the HTML report and the trace show Open / Fields / Submit.
  9. Write beforeAll / beforeEach / afterEach / afterAll, and capture a full-page screenshot only when testInfo.status !== testInfo.expectedStatus.
  10. Use test.describe.serial only when order matters, and leave independent tests outside that block.
  11. Register one Playwright test per data row with a for loop — inline first, then CSV, then JSON, then Faker.
  12. Read login-data.csv through the hand-rolled csvReader.ts, and say out loud that 263 asks for data.expectedURL while the CSV on main has no such column.
  13. Branch on shouldPass === "true" the way 264 does — and notice that 265 compares a JSON boolean to the string "true".
  14. Generate a user with @faker-js/faker and, in 266, admit the password field is filled with testUser.name.
  15. Tell a built-in fixture from a hook from a DDT loop, and refuse to invent test.extend today.

That is the skill. Not the matcher. The skill is prove, prepare, multiply — in that order.

The labs we are actually using

Clone the fundamentals repo and stay on main:

git clone https://github.com/PramodDutta/LearningPlaywrightFundamentals.git
cd LearningPlaywrightFundamentals
git checkout main

Three folders. I fetched every file GitHub lists in them. Those files, as GitHub serves them:

tests/17_Expect_Assertions — prove the page.

  • README.md — module index, value vs locator vs soft vs .not, run commands
  • 256_Expect.spec.ts — value assertions, locator assertions, expect.soft, .not
  • 257_URL_Asserations.spec.ts — title, URL, checked / enabled / visible (Asserations in the filename)
  • Expect_Assertions_Cheatsheet.md — interview one-pager: value, locator, page, API, modifiers
  • More_Expect_Examples.md — long TTA reference (visibility, state, text, a11y, screenshots, poll, timeouts)

tests/18_Test_hooks — prepare the worker and the test.

  • README.md — annotations, test.step, lifecycle, describe.serial
  • 258_Test_HOOK.spec.ts — skip / slow / fixme / fail (HOOK in the filename)
  • 259_Grouped_TEST.spec.ts — three named steps on the practice login form
  • 260_Test_Before_After.spec.ts — beforeAll / beforeEach / afterEach fail screenshot / afterAll
  • 261_Group_Describe.spec.ts — serial checkout logs versus two standalone parallel tests

tests/19_Data_Driven_Testing — multiply the same flow.

  • README.md — inline to CSV to JSON to Faker, plus the three readers
  • 262_DDT_Simple.spec.ts — inline array of five login rows
  • 263_DDT_CSV.spec.ts — readCSV(login-data.csv), asserts toHaveURL(data.expectedURL)
  • 264_DDT_CSV.spec.ts — same CSV, plus hooks, plus shouldPass / expectedError branch
  • 265_DDT_JSON.spec.ts — import loginData from "./registration-data.json" (describe title still says CSV)
  • 266_DDT_FakerJS.spec.ts — one Faker user against TTA Cart login, expects the mismatch alert
  • 267_FakerJS2.spec.ts — Faker-filled practice-profile fields, asserts #submission-output
  • 268_FakerJS3.spec.ts — generateUser() helper, same page, telephone is created and never filled
  • 269_DDT_FakerJS.spec.ts — loop of five Faker registrations across five email domains
  • csvReader.ts — hand-rolled CSV to TestDataRow[]
  • yamlReader.ts — js-yaml parser, optional top-level key — no spec uses it
  • xlsxReader.ts — xlsx sheet_to_json — no spec uses it
  • login-data.csv — seven login rows
  • login-data.yaml — the same seven rows in YAML
  • registration-data.json — five registration rows

I am not opening tests/20_Page_Object_Model today. That is Day 16. I am not inventing a YAML spec. I am not inventing a consuming XLSX spec. I am not inventing test.extend. The 21 README can wait until tomorrow.

Run each module from its README:

npx playwright test tests/17_Expect_Assertions
npx playwright test tests/18_Test_hooks
npx playwright test tests/19_Data_Driven_Testing

One assertion file:

npx playwright test tests/17_Expect_Assertions/256_Expect.spec.ts

One hook file:

npx playwright test tests/18_Test_hooks/260_Test_Before_After.spec.ts

One DDT file, the CSV-plus-hooks spec the 19 README points at:

npx playwright test tests/19_Data_Driven_Testing/264_DDT_CSV.spec.ts

By title, the way the 17 README shows it:

npx playwright test tests/17_Expect_Assertions -g "soft assertions"

Headed, so you can watch the loop:

npx playwright test tests/19_Data_Driven_Testing --headed

Public demo pages on app.thetestingacademy.com need network. Several files still call await page.waitForTimeout(5000). I will point at those lines. I will not pretend they are the production habit. Auto-wait plus expect is the habit. The timeout is a classroom freeze-frame so the batch can see the UI.

Why Day 15 is the next framework decision

Day 10 gave you { page }. That is a fixture. Playwright Test injects it. You did not write chromium.launch().

Day 11–14 taught you what to do *with* that page: locators, session, frames, SVG, files, scroll.

Day 15 asks three framework questions the earlier days could dodge:

  1. How do you fail? expect is the contract. A locator click that never asserts is a script. The cheatsheet in folder 17 is the contract list I want in every PR review.
  2. When do you set up? A hook is not a fixture. beforeEach is a function you own. page is an object Playwright owns. Mixing the words is how juniors start stuffing login into beforeAll and then wonder why the second test has a closed page.
  3. How many times does the same flow run? A for loop that calls test() is data-driven testing. It is not a runtime loop inside one test. Playwright registers the tests when it loads the file. Five rows, five titles, five traces.

In a framework these three become: an assertion helper policy, a hook / fixture layer, and a testdata folder. Today we still read the classroom files. Day 16 puts the login behind LoginPage.ts. Day 21 puts the policy behind a real framework. Do not skip the messy files. The mess is the curriculum.

Look at the diagram. Left card is { page }. Middle card is beforeEach. Right card is for (const data of rows). If you cannot point at which card a line belongs to, you are not ready to write test.extend.

Module 17 — prove it, or it is not a test

I start here because a hook that never asserts is ceremony, and a DDT loop that never asserts is five ceremonies.

The module README is the rule I want you to steal:

Use synchronous generic assertions for plain values. Await locator and page assertions because they auto-retry. Use .not to assert absence. Use expect.soft() when multiple checks should be collected. Prefer state-specific assertions such as toBeChecked(), toBeEnabled(), and toHaveValue() over manual DOM inspection.

Two specs. Two markdown files. No third spec named playwright-assertions.spec.ts. I will say that again when we open More_Expect_Examples.md.

Lab 256 — 256_Expect.spec.ts is three families in one describe

The file opens a describe titled Expect Assertions - TestingAcademy. Three tests. The first test never uses page even though the fixture is in the signature. Classroom leftover. I do not delete the unused { page }. I quote the file.

Test 1 — value assertions. Synchronous. No await.

expect(1 + 2).toBe(3);
// expect(1 + 2).toBe(4);

expect(false).toBeFalsy();
expect(true).toBeTruthy();
expect(null).toBeNull();
expect(34).toBeGreaterThan(11);
expect([1, 2, 3]).toEqual([1, 2, 3]);
expect({ role: 'admin' }).toEqual({ role: 'admin' });
expect({ age: 20, role: 'admin' }).toEqual({ role: 'admin', age: 20 });

toBe is ===. Use it for primitives. The commented toBe(4) is the classroom “this is what failure looks like” line. Leave it commented when you run the module. Uncomment it once in your working copy if you want to see a red value assertion. Put it back.

toEqual is deep equality. The last two object lines are the lesson: key order does not matter. { age, role } equals { role, age }. If you reach for toBe on two object literals, you are comparing references and you will fail a test that is logically green.

toBeTruthy / toBeFalsy / toBeNull / toBeGreaterThan are the generic Jest-style family. They run once. They do not wait for the DOM. If you write expect(await heading.isVisible()).toBe(true) you have thrown away auto-retry. The cheatsheet exists so you stop doing that.

Test 2 — locator assertions. Auto-retrying. await required.

await page.goto('https://app.thetestingacademy.com/playwright/multiple_element_filter.html');

const heading = page.getByText('multiple element filters', { exact: true });
await expect(heading).toBeVisible();
await expect(heading).toContainText('filter', { timeout: 10000 });

const email = page.getByRole('textbox', { name: 'Email Address' });
await expect(email).toHaveAttribute('id', 'email');
await expect(email).toHaveAttribute('type', 'email');
await expect(email).toHaveAttribute('placeholder', 'student@thetestingacademy.com');

const footerLinks = page.locator('footer a');
await expect(footerLinks).toHaveCount(16);

Four jobs in one test:

  1. Visible. The heading multiple element filters is on the page. Exact text. toBeVisible retries until the default timeout — or until you pass { timeout: 10000 } on the next line.
  2. Contains. toContainText('filter') is a substring. toHaveText would be exact. The cheatsheet table at the bottom of that markdown file is this distinction.
  3. Attribute. Three toHaveAttribute calls on the Email Address textbox. Id, type, placeholder. This is better than getAttribute plus a value expect, because the locator assertion retries.
  4. Count. footer a must be 16. If the footer grows, this test fails. That is the point of a count assertion. I do not invent a “greater than 10” here. The file says 16.

If you drop the await on any of those locator lines, Playwright does not wait. The promise is orphaned. The test may pass before the element exists. That is the interview question. The cheatsheet answers it in one row.

Test 3 — soft assertions and negation.

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

const firstName = page.getByLabel('First name');

// Soft: each line records its own failure; test continues either way.
await expect.soft(firstName).toHaveAttribute('id', 'first-name');
await expect.soft(firstName).toBeVisible();
await expect.soft(firstName).toHaveValue('');

// Final hard assertion still runs after the soft block.
await expect(firstName).toBeEnabled();

await page.goto('https://app.thetestingacademy.com/playwright/webtable.html');
await expect(page.locator('#error')).not.toBeVisible();

const title = await page.title();
expect(title).not.toContain('error');

Soft means: collect. Hard means: stop. The three soft lines on First name all run even if the first one fails. The hard toBeEnabled still runs after the soft block. At the end of the test, Playwright fails the test if any soft assertion failed. That is the contract. It is not “soft means ignore.”

Then the file changes page. #error must not be visible on the webtable page. The title string must not contain error. Two .nots: one auto-retrying on a locator, one synchronous on a string you already read. If you write await expect(page.title()).not.toContain('error') you are mixing families. page.title() is a string promise, not a locator. The file does it the honest way: const title = await page.title() then a value expect.

Lab 257 — 257_URL_Asserations.spec.ts is title, URL, and widget state

The filename is 257_URL_Asserations.spec.ts. Asserations. I type it that way in every run command.

The first line of the file is a leftover comment:

// Screenshot assertions (visual diff)

There is no toHaveScreenshot in this spec. The cheatsheet documents toHaveScreenshot. This file does not call it. I will not invent a visual-diff lab for a comment.

Test 1 — URL and title.

await page.goto('https://app.thetestingacademy.com/playwright/widgets/calendar.html');
await page.getByTestId('trigger-depart').click();
await expect(page).toHaveTitle('Calendar Date Picker — The Testing Academy');
await expect(page).toHaveURL('https://app.thetestingacademy.com/playwright/widgets/calendar');

await expect(page).toHaveTitle(/Calendar/);

const appUrl = page.url();
expect(appUrl).toContain('thetestingacademy');

// expect(page.locator('')).toHaveCSS(''); // toHaveClass

Three title/URL lessons:

  1. Exact title. toHaveTitle('Calendar Date Picker — The Testing Academy') is a string. Auto-retrying. Page assertion. await it.
  2. URL without the .html. The goto used calendar.html. The assertion is /playwright/widgets/calendar. That is the URL the widget settles on after the click. I do not “fix” it to add .html. I tell you the file asserts the settled URL.
  3. Regex title. /Calendar/ is the interview form. Exact when the title is a contract. Regex when the suffix changes.

page.url() is a string *now*. The expect(appUrl).toContain('thetestingacademy') is a value assertion. No retry. If you need to wait for a navigation, use await expect(page).toHaveURL(...). Do not snapshot page.url() too early.

The last commented line is a stub for toHaveCSS / toHaveClass. Empty locator. I leave it commented. More_Expect_Examples.md has the real toHaveCSS samples if you want the API, not a passing test.

Test 2 — visible, enabled, checked.

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

const agreeCheckbox = page.getByRole('checkbox', { name: /UFT/ });
const submitBtn = page.getByTestId('profile-submit');

await expect(agreeCheckbox).not.toBeChecked();
await expect(submitBtn).toBeVisible();
await expect(submitBtn).toBeEnabled();

await agreeCheckbox.check();
await expect(agreeCheckbox).toBeChecked();
await page.waitForTimeout(5000);

State-specific matchers. The README already told you to prefer these over reading the DOM yourself.

  • Unchecked first. .not.toBeChecked() on the UFT checkbox.
  • Submit is visible and enabled before you check anything. The button is not gated on the checkbox in this test. I do not invent a disabled-until-checked story. The file never asserts disabled.
  • Then check(), then toBeChecked().
  • Then the classroom waitForTimeout(5000). Production habit is the expects above. The timeout is so the batch can see the tick.

The cheatsheet also documents toBeChecked({ checked: false }) as the positive form of “unchecked.” This spec uses .not.toBeChecked(). Both are valid. I quote what the spec uses.

The cheatsheet — Expect_Assertions_Cheatsheet.md

This file is why folder 17 has more than two specs. It is the interview one-pager. Format on disk: What → How → Example. The opening rule is the one I want above the fold:

Auto-retrying assertions (Locator / Page / APIResponse) MUST be await-ed. Value assertions (numbers, strings, booleans) are synchronous — no await.

I am not pasting the entire markdown into this post as if I wrote it. I am walking the six sections so you know what is *in* the file when you open it on main.

Section 1 — value assertions (non-retrying). toBe, toEqual, toStrictEqual, toBeTruthy / toBeFalsy, toBeNull / toBeUndefined / toBeDefined / toBeNaN, the numeric four, toBeCloseTo, toContain, toContainEqual, toHaveLength, toMatch, toMatchObject, toThrow / toThrowError. Lab 256 uses a subset: toBe, toBeFalsy, toBeTruthy, toBeNull, toBeGreaterThan, toEqual. The rest live in the cheatsheet. I do not invent a toBeCloseTo spec.

Section 2 — locator assertions (auto-retrying). Visibility (toBeVisible / toBeHidden), state (toBeEnabled / toBeDisabled / toBeEditable / toBeEmpty / toBeChecked / toBeFocused / toBeAttached / toBeInViewport), text (toHaveText / toContainText), values (toHaveValue / toHaveValues), attributes (toHaveAttribute / toHaveClass / toHaveCount / toHaveCSS / toHaveId / toHaveJSProperty), screenshot (toHaveScreenshot), a11y (toHaveAccessibleName / toHaveAccessibleDescription / toHaveRole). Labs 256 and 257 use visible, contain-text, attribute, count, checked, enabled. The rest are reference.

Section 3 — page assertions. toHaveTitle, toHaveURL, page-level toHaveScreenshot. Lab 257 uses the first two.

Section 4 — API response. toBeOK. No spec in folder 17 calls request.get. Day 19 of this series is the API layer, and that day uses a different repo. I will not invent an API test in fundamentals folder 17.

Section 5 — modifiers. .not, expect.soft, expect.poll, expect.toPass, expect.configure. Lab 256 uses .not and expect.soft. Lab 255 back on Day 14 already used expect.poll on a lazy list. expect.toPass and expect.configure are cheatsheet-only in this folder.

Section 6 — interview Q&A table. Six rows. Difference between toBe and toEqual. Why await locator assertions. Soft vs hard. expect.poll vs expect.toPass. toHaveText vs toContainText. Default assertion timeout (5 seconds, via expect.configure or playwright.config.ts). I steal four of those rows into the FAQ at the bottom of this post, attributed to this file.

Closing tip on the cheatsheet:

All Locator/Page assertions auto-retry. Never wrap them in try/catch for retry logic — use expect.toPass.

That sentence is the difference between a junior suite and a review I will approve.

The long reference — More_Expect_Examples.md

The second markdown is the classroom encyclopedia. Title on disk: PLAYWRIGHT ASSERTIONS & EXPECTATIONS — COMPLETE REFERENCE. It walks visibility, state, text, attributes, values, count, accessibility, screenshots, page, API, generic values, and modifiers. The examples hit playwright.dev, the-internet.herokuapp.com, demo.playwright.dev/todomvc, and reqres.in. Those are documentation samples inside a markdown file. They are not specs in this folder.

The run command at the top of that file is:

npx playwright test playwright-assertions.spec.ts -g "<test-name>"

There is no playwright-assertions.spec.ts on main under tests/17_Expect_Assertions. I will not invent it. If you want to run assertions, you run 256_Expect.spec.ts and 257_URL_Asserations.spec.ts. The README already gave you those two commands.

The bottom of More_Expect_Examples.md recommends a filename PLAYWRIGHT_ASSERTIONS_COMPLETE_REFERENCE.md. That file is also not in the tree. The file you are reading is More_Expect_Examples.md. Classroom leftover. Follow the GitHub name.

The rule-of-thumb block at the end of that file is worth stealing into your notes:

  • Locator/Page assertions auto-retry until timeout.
  • Generic assertions execute once immediately.
  • .soft continues after failures.
  • expect.poll polls non-locator values.
  • Prefer toHaveCount(0) over .not.toBeVisible() for lists.

Lab 256’s #error .not.toBeVisible() is the “element should not appear” case. A list that should be empty is a count. Different question. Different matcher.

The long file also shows toMatchAriaSnapshot, toBeInViewport({ ratio: 0.5 }), toHaveJSProperty, toBeAttached({ attached: false }), and a TodoMVC flow that adds two items and asserts toHaveCount(2). None of those are a third spec. They are the encyclopedia. Open the markdown. Do not ask me for a file named after the recommended title.

Module 18 — prepare it, or you will paste goto forever

Folder name: 18_Test_hooks. Four specs. The README’s pattern list is the policy:

  • Annotations when behavior depends on browser support, known defects, or expected failures.
  • test.step() to make reports readable without splitting one scenario.
  • beforeEach for repeated page setup; beforeAll / afterAll for worker-level work.
  • afterEach plus testInfo.status / testInfo.expectedStatus for artifacts only on unexpected failure.
  • test.describe.serial() only when tests must share ordering.

Day 10 already showed annotations in Lab210_Test_Annoations.spec.ts, including a live test.only I told you not to commit. Lab 258 is the cleaner classroom version. I still follow 258 as GitHub serves it.

Lab 258 — 258_Test_HOOK.spec.ts is annotations, not lifecycle hooks

The filename says HOOK. The body is test.skip / test.slow / test.fixme / test.fail. Lifecycle hooks are lab 260. I do not merge the two files because the folder is named hooks.