Skip to contentPlayForge
Act IChapter 5
chapter 5

5. Your First Playwright Test

Install Playwright, run and record a test, read it line by line, and clone your framework.

25m read10m practice · 4 exercises35m in total

Chapter film (about 7m) is coming.

Checkpoint 1 of 4

Your First Playwright Test: Install, Run, Read the Report

What you are about to do. Create a Playwright project, run the two example tests it comes with, and read the report. Do this lesson with your terminal open. Every command here was run on Windows against Playwright 1.64. You need Node set up first; the Set up your machine lesson in the Welcome module, before chapter 1, covers that.

1. Create the project. In your terminal, make a new folder, move into it, and run the Playwright installer:

mkdir my-first-tests cd my-first-tests npm init playwright@latest

It asks four questions. Use the arrow keys and Enter:

  • TypeScript or JavaScript? Choose JavaScript. That is the language this course teaches. Many teams use TypeScript, which is JavaScript with type labels added, and you will meet it at the end of the course, in TypeScript: Convert Your Framework.
  • Where to put your end-to-end tests? Press Enter to accept tests.
  • Add a GitHub Actions workflow? Type n for now (the default is Yes, so don't just press Enter). That is for running tests on a server; the CI/CD with GitHub Actions lesson covers it, much later.
  • Install Playwright browsers? Answer Y. This downloads Chromium, Firefox and WebKit (the engine behind Safari). It is a few hundred MB, so give it a few minutes.

2. Look at what it made. Open the folder in VS Code (code .):

  • tests/example.spec.js holds two example tests. A spec file is a file of tests.
  • playwright.config.js holds the settings: where tests live, which browsers to use, and what report to produce.
  • package.json lists the project's libraries. @playwright/test is the one that matters.
  • node_modules/ is the downloaded libraries. Never edit it.

3. Run the tests. npx runs a command that comes with a library your project has installed. Here that library is Playwright.

npx playwright test

The example tests open playwright.dev, so you need an internet connection. You will see 6 passed: two tests, each run in three browsers, because the config lists Chromium, Firefox and WebKit as separate projects. The browsers run headless, with no window, which is why you didn't see anything open. To run in one browser only, use npx playwright test --project=chromium.

4. Read the report. Run npx playwright show-report. A page opens in your browser listing every test, how long it took, and every step inside it. When a test fails, this is the first place you look. Playwright opens this report on its own after a failed run, and the terminal then seems to hang; it is waiting for you to press Ctrl+C in the terminal, which stops the report server.

5. Watch it work. Run npx playwright test --ui. UI mode opens a window where you can run one test at a time and step through each action with a screenshot of the page at that moment. If you only try one thing in this lesson, try this.

6. Read the test. The box below is tests/example.spec.js, exactly as generated. Read it line by line. test(...) names a test. page.goto opens a URL. page.getByRole('link', { name: 'Get started' }) finds the link the way a user would, by what it is and what it says. expect(...) is the check, the "expected result" column of your manual test case. The next lesson, Reading a Test Line by Line, explains every word of it.

A word on codegen. npx playwright codegen https://playwright.dev opens a browser and writes test code as you click. It is a good way to see how actions turn into code. But a recorder writes down what you did, not what you meant to check, and that is the same trap AI-written tests fall into. Treat its output as a first draft you review, never as a finished test. Codegen: Record a Test, Then Read It Critically, two lessons on, shows how.

If something goes wrong:

  • Executable doesn't exist means the browsers didn't download. Run npx playwright install.
  • Only the Chromium tests pass means only Chromium was installed. Run npx playwright install to add the other two browsers.
  • running scripts is disabled on this system in PowerShell: see the fix in Set up your machine, or use Command Prompt.
  • Installs fail at the office: a corporate proxy is the usual cause. Try from home, or ask a developer for the npm proxy setting.
javascriptruns against a real browser
// @ts-check import { test, expect } from '@playwright/test'; test('has title', async ({ page }) => { await page.goto('https://playwright.dev/'); // Expect a title "to contain" a substring. await expect(page).toHaveTitle(/Playwright/); }); test('get started link', async ({ page }) => { await page.goto('https://playwright.dev/'); // Click the get started link. await page.getByRole('link', { name: 'Get started' }).click(); // Expects page to have a heading with the name of Installation. await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); });
Finished reading? Tick it off.

Sign in to track your progress

Checkpoint 2 of 4

Reading a Test Line by Line

In plain English. A Playwright test is a short piece of code with a name. Inside it you do what a manual tester does: open a page, find something on it, act on it, check what you see. Five pieces appear in nearly every test for the rest of this course: import, test, { page }, await and expect. This lesson names each one and says what it is for. You do not need to know how they work inside.

The whole test. It opens the Meridian Ops home page, clicks the Catalog link, and checks that the page which opens has a heading called Catalog. A manual tester would write that as three steps. To try it, save the box as tests/read-me.spec.js in the my-first-tests folder from the last lesson and run npx playwright test read-me. It runs once in each browser, as before.

import { test, expect } from '@playwright/test'; Playwright's tools live in a package your project installed. This line brings two of them into the file: test, which declares a test, and expect, which checks something. It goes at the top of every test file. The Modules: require() vs import/export lesson, a few chapters on, explains import properly. For now, copy it unchanged.

test('the Catalog link opens the catalog page', async ({ page }) => { ... }); test takes two things: a name and a function. The name is a plain sentence. It is what you will read in the report when the test fails, so write it like the title of a manual test case. The function holds the steps.

{ page }. Playwright hands every test a brand-new, empty browser tab and calls it page. Nothing is left in it from any other test: no cookies, no signed-in user. The curly braces pick page out of what Playwright passes in, the same way the Objects & Destructuring lesson pulled a value out of an object. Everything you do in the browser goes through page.

async and await. Browsers take time. A page loads, a click lands, the screen redraws. So every Playwright method that touches the browser hands back a promise, which is a note saying "I will tell you when this is done". await means "stop on this line until that note comes back, then go on". async in front of the function is the permission slip: await only works inside an async function. The rule for now: start the test function with async, and start every line that talks to the browser with await. The Callbacks & Promises and Async/Await Mastery lessons in the async chapter explain what happens underneath.

Leaving an await off is the most common first mistake, and it does not always fail the same way. Left off page.goto or a click, the test may still pass, because the check that follows keeps retrying. That is luck, not safety. Left off expect, the test fails with the message Test ended, even when the page is right.

page.goto('https://practice.learnplayforge.com/?mock=static&seed=42') opens the address and waits for the page to load. The two settings after the ? tell Meridian Ops to answer from fixed data, so the page looks the same on every run. You will keep them on every address for the practice site.

page.getByRole('link', { name: 'Catalog' }) finds an element the way a person would: by what it is (a link) and by what it says (Catalog). This is a locator. It is a description of where to look, not the element itself. Playwright searches the page's live structure, called the DOM, each time you use the locator, and nothing happens until you add an action such as .click(). The getByRole and the Accessible Name lesson goes through it in detail.

await expect(...).toBeVisible(). expect is the "expected result" column of your manual test case. You give it the thing to check, then say what you expect: here, that the heading is on screen. The check waits. By default it keeps trying for up to five seconds before it fails, so a page that is a little slow does not fail the test. When it does fail, the report names the locator it looked for and says it found nothing. See Assertions on playwright.dev.

Practise this. Reading short tests, saying what each line does, and spotting the missing await.

javascriptruns against a real browser
import { test, expect } from '@playwright/test'; test('the Catalog link opens the catalog page', async ({ page }) => { await page.goto('https://practice.learnplayforge.com/?mock=static&seed=42'); await page.getByRole('link', { name: 'Catalog' }).click(); await expect(page.getByRole('heading', { name: 'Catalog' })).toBeVisible(); });
Finished reading? Tick it off.

Sign in to track your progress

Checkpoint 3 of 4

Codegen: Record a Test, Then Read It Critically

In plain English. Playwright can watch you use a website and write the test code for what you did. That tool is called codegen. It is a quick way to see how clicks and typing turn into code, and it gets you a first draft in a minute. But a recording is only what you did. It has no idea what you wanted to prove. So the skill is not recording. It is reading the recording and deciding what is missing.

Record one. In your my-first-tests folder, run:

npx playwright codegen "https://practice.learnplayforge.com/?mock=static&seed=42"

Keep the quotes: without them, some terminals treat the & as the end of the command. Two windows open: a browser, and the Playwright Inspector with the code so far. Click Sign in at the top right. Click the Work email field and type clerk@meridian.test. Click Password and type Passw0rd!. Click the Sign in button. Watch the code grow as you go, then copy it from the Inspector into tests/recorded.spec.js.

One trap. If you save with -o tests/recorded.spec.js instead, add --target playwright-test as well. A file name ending in .js on its own gives you the older library style, which does not run under npx playwright test.

What it wrote. Run on Playwright 1.64, this flow produced:

import { test, expect } from '@playwright/test'; test('test', async ({ page }) => { await page.goto('https://practice.learnplayforge.com/?mock=static&seed=42'); await page.getByTestId('login-link').click(); await page.getByTestId('login-email').click(); await page.getByTestId('login-email').fill('clerk@meridian.test'); await page.getByTestId('login-password').click(); await page.getByTestId('login-password').fill('Passw0rd!'); await page.getByTestId('login-submit').click(); });

Yours may differ in small ways, but the shape will be the same. Run it with npx playwright test recorded. It passes. That is the problem.

Read it critically. Ask four questions of any recorded test, and of any test an AI agent hands you.

1. What does it check? Nothing. There is no expect in it. Change the password to wrong-password and run it again: it still passes, because it clicks Sign in and never looks at what happened. A recording is a replay of your steps, not a test. A test is a replay plus a check.

2. Does the name say what it proves? 'test' says nothing. In a report of two hundred tests you could not find it. Name it for the behaviour: a clerk can sign in and sees their own name.

3. Is every step needed? Each field is clicked and then filled. fill already focuses the field before it types, as the Actions: Clicking, Typing & Forms lesson will show, so the click adds nothing. The first click, on the Sign in link, is only the route to the page. If the test is about signing in, start on the sign-in page.

4. Would you choose these locators? Meridian Ops marks its fields with test ids, so codegen used getByTestId for every step. That works, but a test id is a label for developers. The user sees the words Work email. A locator built from what the user sees breaks less often and reads like the manual test case; the getByText, getByLabel, getByPlaceholder, getByTestId lesson covers when each one is right.

Two smaller things. The address and the password are typed into the file. Real passwords never belong there; the Secrets, and the Repository You Push lesson shows where they go. The https://... address is also repeated in every test; the playwright.config.ts Complete Reference lesson shows how to write it once.

Adding a check from the recorder. You do not have to add checks by hand. The recorder's toolbar has buttons named Assert visibility, Assert text and Assert value. Click one, then click the element on the page, and codegen adds an expect line for it. It is still your job to choose what is worth checking.

The reviewed test. Below is the recording after those four questions: same journey, one real check, a name that says what it proves, and the sign-in page as the starting point. Change the password to wrong-password and this version fails, which is what a test is for.

Practise this. Reading recorded tests, and saying what each one fails to prove.

javascriptruns against a real browser
import { test, expect } from '@playwright/test'; test('a clerk can sign in and sees their own name', async ({ page }) => { await page.goto('https://practice.learnplayforge.com/login.html?mock=static&seed=42'); await page.getByLabel('Work email').fill('clerk@meridian.test'); await page.getByLabel('Password').fill('Passw0rd!'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page.getByTestId('current-user')).toHaveText('Devraj Menon'); });
Finished reading? Tick it off.

Sign in to track your progress

Checkpoint 4 of 4

Build: Clone the Framework, Run It, and Break a Test

In plain English. From here to the end of the course you grow one project, the PlayForge framework, a little at a time. This lesson gets it onto your machine, runs its two tests against Meridian Ops, and then breaks one on purpose. Reading a failure is a skill you use every working day, and it is easier to learn on a failure you caused than on one that arrives at five in the evening.

Build. Get the project. You need Git for this step. Git is the tool teams use to keep versions of a project; a later chapter teaches it properly. For now it is a download command. Type git --version. If it prints a version, carry on. If not, install Git from git-scm.com and open a new terminal. Then run:

git clone https://github.com/jatin-thacker/playforge-framework.git cd playforge-framework git checkout ch05-end

The address is the PlayForge framework repository on GitHub, github.com/jatin-thacker/playforge-framework. It is public: you need no account to download it. The checkout line matters. The repository holds the finished framework from the end of the course, and you want the small starting version. The tag ch05-end is the project as it stands at the end of this chapter. Git will print a long note about "detached HEAD". It only means you are looking at a tagged version rather than a branch; ignore it. Each later Build lesson names the tag for its own chapter, so you can always compare your copy with a known good one.

Install it.

npm ci npx playwright install chromium

npm ci installs exactly the library versions the project lists in package-lock.json. Everyone on the course gets the same versions, so a failure on your machine is unlikely to come from a different version. The second line downloads the Chromium browser. It takes a minute.

Look around. Open the folder in VS Code. Three things to find:

  • tests/smoke/home.spec.js and tests/smoke/login.spec.js are the two tests. Smoke means a short, quick check that the basics work.
  • playwright.config.js holds the settings, as in your first project. It runs Chromium only.
  • config/.env.prod holds one line, BASE_URL, the address of Meridian Ops. Tests write page.goto('/...') without the address, and Playwright adds this one in front. The playwright.config.ts Complete Reference lesson explains how.

Open home.spec.js, shown in the box below. It is the kind of test you read in Reading a Test Line by Line, with one new word: test.describe groups tests under a heading in the report.

Run it.

npx playwright test

It opens Chromium without a window, runs both tests against the live practice site, and ends with:

  2 passed (3.1s)

The time will differ. The count should not. If you see Executable doesn't exist, run npx playwright install chromium again.

Break it. In home.spec.js, change the heading name from 'Operations dashboard' to 'Operations dashbord', with a letter missing. Save, and run the tests again. This time the output ends in 1 failed and 1 passed. Read the failure from the top.

  • The headline. Error: expect(locator).toBeVisible() failed names the check that failed.
  • The facts. Locator, Expected, Timeout and Error: element(s) not found say what it looked for, what it wanted, how long it waited (five seconds) and what it found: nothing.
  • The call log. The lines under Call log are what Playwright did, in order. Here it only waited.
  • The code. The numbered lines show your own file, with an arrow > at the line that failed.

Now a different failure. Put 'Operations dashboard' back. In login.spec.js, change 'Devraj Menon' to 'Devraj Menen' and run again. The failure now says:

Expected: "Devraj Menen"
Received: "Devraj Menon"

This time the element was found, and its text was wrong. In the call log, unexpected value "Devraj Menon" shows what it held. Both failures point at one line of your code, and the facts above it tell you which kind of mistake you made: the thing was not there, or it was there with something else in it. When a test fails on a real run, that is the first question to ask.

After a failure, Playwright either opens the HTML report on its own, and the terminal waits, or prints the command npx playwright show-report. If it is waiting, press Ctrl+C. Run npx playwright show-report whenever you want to look at the report. Then put 'Devraj Menon' back and run npx playwright test once more. It should end in 2 passed.

What you have now. A working project that matches the tag ch05-end, two tests you can run, and two failures you have read. The next chapters teach what is under these tests. The project grows from here.

Practise this. Reading a failure message and saying which line broke and why.

javascriptruns against a real browser
import { test, expect } from '@playwright/test'; test.describe('Smoke tests - Home page', () => { test('opens the home page and asserts the operations dashboard heading is visible', async ({ page }) => { // ?mock=static makes Meridian Ops answer from fixed files instead of a service worker, // which reloads the page once on a fresh browser. seed=42 makes its data the same every run. // The app carries both parameters across its own links, so the redirect after sign-in keeps them. await page.goto('/?mock=static&seed=42'); // Assert that the dashboard heading is visible using user-facing role locator const heading = page.getByRole('heading', { name: 'Operations dashboard' }); await expect(heading).toBeVisible(); }); });
Finished reading? Tick it off.

Sign in to track your progress