Target audience: Complete beginners — no Playwright, no TypeScript knowledge assumed.
Format: Short, focused lessons with one takeaway each. Libraries installed incrementally as they're introduced.
Application Under Test: https://www.saucedemo.com — an official testing website provided by Sauce Labs (the company behind the cross-browser testing platform). It's a mock e-commerce site built specifically for practicing test automation with known users and deliberate error states.
| Lesson | Topic | Time |
|---|---|---|
| 1 | What is Playwright? | 15 min |
| 2 | Environment Setup | 30 min |
| 3 | What Did That Command Create? | 20 min |
| 4 | Your First Playwright Test | 55 min |
| 5 | Test Runner Basics | 40 min |
| 6 | Locators | 65 min |
| 7 | iframes & Shadow DOM | 20 min |
| 8 | Actions | 35 min |
| 9 | Assertions & Auto-Waiting | 45 min |
| 10 | Test Structure & Hooks | 25 min |
| 11 | Page Object Model | 60 min |
| 12 | Custom Fixtures | 25 min |
| 13 | Environment Variables (dotenv) | 20 min |
| 14 | BDD (playwright-bdd) | 65 min |
| 15 | Allure Reporting | 25 min |
| 16 | Cleaning Up the Config | 15 min |
| 17 | Full Framework & Best Practices | 30 min |
| 18 | Running Tests | 10 min |
| Total | ~10 hours |
Time: 15 min
- What is Playwright? — an open-source tool from Microsoft for automating web browsers. You write code that controls a browser just like a human would: clicking buttons, typing text, reading page content. Official docs: https://playwright.dev/docs/intro
- Why automate tests?
- Manual testing is slow and repetitive — you'd have to log in, click around, and check results by hand every time
- Automated tests run in seconds and catch regressions before they reach users
- They run the same way every time — no human error or fatigue
- What can Playwright do?
- Run tests in Chromium (Chrome/Edge), Firefox, and WebKit (Safari) — one API, three browsers
- Take screenshots, record videos, capture network traffic
- Run tests in parallel — 10 tests finish as fast as the slowest one, not the sum of all 10
- Generate traces (a full debug log of every action, network request, and console message)
- What you'll build — by the end of this course, you'll have a professional test framework with:
- Page Object Model (POM) — clean, reusable test code
- BDD (Gherkin) — plain-English test scenarios
- Custom fixtures — reusable test setup
- Allure reporting — rich, interactive test reports
- CI-ready configuration
Time: 30 min
Install: Node.js, npm, VS Code or Cursor
-
What is Node.js? — a JavaScript runtime. It lets you run JavaScript on your computer (not just in a browser). Playwright is a Node.js library.
-
What is npm? — Node Package Manager. It downloads and manages libraries (dependencies) for your project.
-
Install Node.js and npm (if not already installed):
- Go to https://nodejs.org (the LTS version is recommended)
- Download the Windows installer (.msi) and run it
- Follow the installer — leave all defaults checked (it adds
nodeandnpmto your PATH automatically) - After installation, restart your terminal (or VS Code/Cursor) so the new PATH takes effect
-
Install an editor — you need a code editor to write and manage your test files. Two good free options:
- VS Code — download from https://code.visualstudio.com. The installer is straightforward — keep all defaults.
- Cursor — download from https://www.cursor.com. It's built on VS Code with AI features built in. Same look and feel.
-
Verify everything is installed — open a terminal (Command Prompt, PowerShell, or your editor's built-in terminal) and run:
node --version # should show v20 or higher npm --version # should show 10 or higher code --version # if using VS Code — should show a version number
If
nodeornpmis not recognized, restart your terminal and try again. If it still fails, restart your computer so the PATH change takes effect. -
Create the project — one command scaffolds everything:
mkdir playwright-ui-testing cd playwright-ui-testing npm init playwright@latest- You'll be asked: "Do you want to use TypeScript?" — select Yes
- "Where to put your tests?" — keep the default
tests/ - "Add a GitHub Actions workflow?" — No (we'll cover CI separately)
- "Install Playwright browsers?" — Yes (downloads Chromium, Firefox, WebKit)
-
What just happened? — the command created:
package.json— lists dependencies and scriptsplaywright.config.ts— central configuration filetests/— folder with a sample testnode_modules/— downloaded libraries (don't touch this)- Browsers installed in a system cache
-
Open in your editor:
- VS Code: run
code .in the project folder, or open VS Code and use File → Open Folder - Cursor: run
cursor .in the project folder, or open Cursor and use File → Open Folder
- VS Code: run
Time: 20 min
After running npm init playwright@latest, you'll see these files/folders. Here's what each one is:
-
playwright.config.ts— the main configuration file. It tells Playwright where your tests are, which browsers to use, and other settings like timeouts and screenshots. -
package.json— the project manifest. It lists the libraries your project depends on (@playwright/test), scripts you can run (npx playwright test), and metadata about the project. -
node_modules/— the folder where npm downloads and stores all the installed libraries. You never edit this folder manually. It should stay in.gitignore. -
tests/— the default folder where Playwright looks for test files (files ending in.spec.tsor.test.ts). You can change this in the config. -
playwright-report/— generated after a test run; contains the HTML report you can open in a browser. -
tsconfig.json(optional) — TypeScript configuration.npm init playwright@latestmay not generate this file. If you want to add TypeScript-specific settings (strict mode, custom include paths), create it at the project root:{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "lib": ["ES2022"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "outDir": "./out", "types": ["node"] }, "include": ["src/**/*.ts", "playwright.config.ts"], "exclude": ["node_modules"] }Key settings:
target: ES2022,module: commonjs— compiles modern TypeScript to Node-compatible JavaScriptstrict: true— catches more errors at compile time (e.g., null checks)include— which files TypeScript type-checks (your source files + config)- You rarely touch this file, but it's essential for IDE type-checking.
-
.gitignore— tells Git which files/folders to ignore.What is Git? Git is a tool that tracks changes to your files and lets you upload your project to hosting platforms like GitHub or GitLab. This means your code lives on the cloud — safe if your laptop breaks, easy to share with teammates, and you can rewind to any previous version if something breaks.
.gitignoretells Git "don't upload these files" — things like secrets (.env), downloaded libraries (node_modules/), and generated reports. Thenpm initcommand creates a basic.gitignore. By the end of this course it will look like this:node_modules/ /playwright-report/ /test-results/ /allure-results/ /allure-report/ /.features-gen/ /playwright/.cache/ /playwright/.auth/ /.envGenerated folders (
allure-results/,.features-gen/,playwright-report/) don't need to be in Git — they're rebuilt when you run tests. Secrets (.env) stay local. Don't worry about memorizing these now; each entry will make sense when we add the tool that creates it.
Time: 55 min
-
What is a test file? A file ending in
.spec.tsor.test.ts..tsmeans TypeScript (JavaScript with types). Playwright automatically finds these files and runs them. -
Walk through the simplest test line by line:
import { test, expect } from '@playwright/test';
This pulls in the two things you need:
test(to define a test) andexpect(to check things).test('has title', async ({ page }) => {
test('has title', ...)— defines a test named "has title"async— this function will useawaitinside (needed for operations that take time, like loading a page)({ page })— Playwright gives your test apageobject. Thepagerepresents a browser tab. The curly braces{}withpageinside is called destructuring — you're pulling thepagefixture out of the built-in set of tools Playwright provides.
await page.goto('https://www.saucedemo.com');
page.goto(...)— tells the browser tab to navigate to a URLawait— wait for the page to finish loading before moving to the next line- Application Under Test: we're using https://www.saucedemo.com — an official testing website provided by Sauce Labs (the company behind the cross-browser testing platform). It's a mock e-commerce site built specifically for practicing test automation. It has known users, expected behaviors, and deliberate error states — perfect for learning without needing your own app.
test('has title', async ({ page }) => { await page.goto('https://www.saucedemo.com'); await expect(page).toHaveTitle('Swag Labs'); });
expect(page)— "I expect this page to have a certain property".toHaveTitle('Swag Labs')— check that the page's<title>tag matches "Swag Labs"- If it doesn't match, the test fails
-
Set
baseURL— instead of writing the full URL in every test, set it once inplaywright.config.ts:export default defineConfig({ use: { baseURL: 'https://www.saucedemo.com' }, });
Now tests can use relative paths:
await page.goto('/'); // same as 'https://www.saucedemo.com'
One change in config updates every test — much cleaner.
-
Run it:
npx playwright testBy default Playwright configures 3 projects — Chromium, Firefox, and WebKit — so your single test runs three times, once in each browser. You'll see output like:
✓ 1 [chromium] › test.spec.ts:3:6 › has title ✓ 2 [firefox] › test.spec.ts:3:6 › has title ✓ 3 [webkit] › test.spec.ts:3:6 › has titleThis is great for cross-browser coverage, but for now keep things simple by removing Firefox and WebKit and keeping only Chromium. Open
playwright.config.tsand replace theprojectsblock with:projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'], viewport: { width: 1920, height: 1080 }, }, }, ],
Setting the viewport ensures your tests always run at a consistent screen size, so layout-dependent assertions don't randomly fail on smaller windows.
import { defineConfig, devices } from '@playwright/test';
Now your test runs only in Chromium, once.
npxruns a command from the local project (not globally installed)- If it passes, you get a green checkmark. If it fails, you get a red X with details on what went wrong.
-
What you're seeing in the terminal is the list reporter — it prints each test name and result as they run. Playwright also generates an HTML report in
playwright-report/that you can open withnpx playwright show-report.Configure reporters in
playwright.config.ts:reporter: [ ['list'], // live terminal output ['html'], // browsable HTML report ],
You can add more reporters later (like Allure) without removing these.
-
View the HTML report:
npx playwright show-report- This opens
playwright-report/index.htmlin your browser - Shows all tests, pass/fail status, how long each took, and error details on failures
- Only opens manually — Playwright doesn't auto-open it when all tests pass
- This opens
-
Playwright VS Code extension — install "Playwright Test for VS Code" by Microsoft from the marketplace. It adds a testing sidebar where you can:
- Run or debug individual tests with one click (no terminal commands)
- See pass/fail status inline in your test file
- Pick locators by clicking elements in the browser
- Record test actions (codegen) directly from VS Code
- Great for beginners — less terminal, more visual feedback
Time: 40 min
-
test()— defines a test case.describe()— groups related tests together.import { test, expect } from '@playwright/test'; describe('Login', () => { test('works with valid credentials', async ({ page }) => { await page.goto('https://www.saucedemo.com'); // ... test steps }); test('fails with wrong password', async ({ page }) => { await page.goto('https://www.saucedemo.com'); // ... test steps }); test('fails with wrong password', async ({ page }) => { await page.goto('https://www.saucedemo.com'); // ... test steps }); });
-
Tagging non-BDD tests — add tags to tests so you can filter them in the HTML report or CLI. Pass a
tagoption totest():test('works with valid credentials', { tag: ['@smoke', '@core', '@non-bdd-tests'] }, async ({ page }) => { // ... }); test('fails with wrong password', { tag: ['@extended', '@non-bdd-tests'] }, async ({ page }) => { // ... });
Tags appear in the HTML report filter. Run by tag:
npx playwright test --grep "@smoke" -
expect()— makes an assertion. If it's false, the test fails. Common matchers:expect('hello').toBe('hello'); // exact match expect('hello world').toContain('world'); // partial match expect(2 + 2).toEqual(4); // deep equality (objects, arrays) expect(true).toBeTruthy(); // truthy check
-
async/await— Playwright actions take time (loading pages, waiting for elements).awaitpauses until the action finishes:// Wrong (test will fail before navigation completes): test('wrong', ({ page }) => { page.goto('https://www.saucedemo.com'); // missing await }); // Right: test('right', async ({ page }) => { await page.goto('https://www.saucedemo.com'); });
-
Demo: write a file with 3 tests — 2 that pass, 1 that fails on purpose — and watch the output show which passed and which didn't
Time: 65 min
-
What is a locator? (a way to tell Playwright which element on the page to interact with)
-
Playwright's own locators — preferred because they're built into the framework, resilient to DOM changes, and mimic how users find things:
getByRole— finds elements by their ARIA role (button, heading, link, textbox, etc.):<button>Login</button>
await page.getByRole('button', { name: 'Login' }).click();
This is the #1 recommended locator. It finds the button by its role (
button) and its accessible name (Login). Works for links ('link'), text inputs ('textbox'), checkboxes ('checkbox'), headings ('heading'), and more.getByText— finds by visible text content:<div>Welcome back, User!</div>
await expect(page.getByText('Welcome back')).toBeVisible();
Useful for paragraphs, divs, spans — anything where you can see the text but there's no label or role.
getByLabel— finds form inputs by their<label>element:<label for="email">Email</label> <input id="email" />
await page.getByLabel('Email').fill('user@example.com');
Perfect for form fields. The
<label>can wrap the input or use theforattribute — Playwright handles both.getByPlaceholder— finds inputs by their placeholder text:<input placeholder="Enter your name" />
await page.getByPlaceholder('Enter your name').fill('Alice');
Good for login forms and search bars where placeholders are unique.
getByTestId— finds by a custom data attribute (requires config):<button data-testid="submit-btn">Submit</button>
await page.getByTestId('submit-btn').click();
Most stable but requires developers to add
data-testidattributes. Playwright usesdata-testidby default — no config needed. If your team uses a different attribute (e.g.,data-testordata-cy), set it inplaywright.config.tsundertestIdAttribute: -
Fallback locators — CSS and XPath, use when Playwright locators can't express what you need:
page.locator('#username'); // CSS by id page.locator('.card'); // CSS by class page.locator('button.primary'); // CSS by tag + class page.locator('[data-type="user"]'); // CSS by any attribute page.locator('xpath=//button[text()="Login"]'); // XPath
-
Chaining & filtering — narrow down when multiple elements match:
page.getByRole('listitem').filter({ hasText: 'Apple' }); page.locator('.product-row').filter({ has: page.getByRole('button') }); page.getByRole('button').first(); page.getByRole('button').last(); page.getByRole('button').nth(2);
-
Why prefer Playwright locators over CSS/XPath?
- Auto-waiting — they wait for the element to be visible and enabled before acting
- Accessibility-first — they find elements the way users and screen readers do
- Less brittle — a CSS class change won't break
getByRole, but will break.locator('.btn-primary')
-
Playwright Test Generator (codegen) — if manually writing locators feels overwhelming, Playwright can generate the code for you. You just click around in a browser, and it writes the test.
How to use it:
- Run this command in your terminal:
npx playwright codegen https://www.saucedemo.com
- Two windows appear side by side:
- Left: the browser — you interact with the page normally (click, type, select)
- Right: the code panel — Playwright writes the equivalent test code in real time
- Every action you take generates a line of code. For example:
- Click the Username field →
page.getByPlaceholder('Username').click() - Type
standard_user→page.getByPlaceholder('Username').fill('standard_user') - Click the Login button →
page.getByRole('button', { name: 'Login' }).click()
- Click the Username field →
- Once you've recorded the flow, copy the code from the panel and paste it into your test file
What if you make a wrong click? — just clear the generated code with the "Clear" button in the code panel and start over. No harm done.
Using codegen from VS Code (or Cursor) — the Playwright VS Code extension (installed in Lesson 4) has codegen built in:
- Open a test file
- Right-click anywhere in the file
- Select "Record at cursor" — the same browser + code panel opens, but the generated code is inserted directly into your file at the cursor position
When to use codegen:
- Learning what locators Playwright finds for an element — instead of guessing, just click it and see what Playwright picks
- Quickly prototyping a test scenario — generate the rough flow, then clean it up later
- When you're stuck on a tricky element — codegen almost always finds a working locator
Tip: codegen generates working code, but it tends to be verbose with extra
click()calls. Use it as a starting point — then simplify by removing unnecessary steps and using direct actions likefill()instead ofclick()+type().Try it now: run
npx playwright codegen https://www.saucedemo.com, log in withstandard_user/secret_sauce, and watch Playwright write the entire login test for you in seconds. - Run this command in your terminal:
-
Demo: open a real page (e.g., saucedemo.com), use DevTools to inspect elements, write locators for username field, password field, and login button using
getByRole,getByPlaceholder, andgetByLabel. Then chain locators to find specific items in a list.
Time: 20 min
-
iframes: an
<iframe>is a whole HTML page embedded inside another page. Normal locators onpagecan't see inside it.Example page with an iframe:
<iframe id="payment-widget" src="https://checkout.example.com"></iframe>
// Step 1 — create a FrameLocator targeting the iframe const paymentFrame = page.frameLocator('#payment-widget'); // Step 2 — use the same locator methods on the frame await paymentFrame.getByPlaceholder('Card number').fill('4242 4242 4242 4242'); await paymentFrame.getByPlaceholder('MM/YY').fill('12/28'); await paymentFrame.getByRole('button', { name: 'Pay' }).click();
Key points:
frameLocatorsupports ALL the same locators:getByRole,getByText,getByLabel,getByPlaceholder,locator(), etc.- You can chain like
page.frameLocator('#a').frameLocator('#b')for nested iframes - For a plain
<iframe>withoutidorsrc, usepage.frameLocator('css=iframe')
-
Shadow DOM: encapsulated DOM attached to a regular element (used by web components). Playwright handles it automatically — no extra code needed.
<my-button>Submit</my-button> <script> class MyButton extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = `<button>${this.textContent}</button>`; } } customElements.define('my-button', MyButton); </script>
// Playwright sees through shadow DOM automatically: await page.getByRole('button', { name: 'Submit' }).click(); // This works even though <button> is inside the shadow root
Important: this only works with open shadow DOM (
mode: 'open'). If the component usesmode: 'closed', Playwright can't reach inside — the element is invisible to locators and even>>won't help. In that case:- Ask the developers to switch to
mode: 'open'(it's the recommended setting for testability) - If that's not possible, use
page.evaluate()to access the element via JS directly
When Playwright's auto locators don't work on open shadow DOM, you can use the
>>combinator — a Playwright-specific CSS syntax that pierces through open shadow DOM in one selector:await page.locator('my-button >> button').click();
This is equivalent to chaining locators but written as a single CSS string. Again, only works on open shadow DOM.
- Ask the developers to switch to
-
Demo: saucedemo.com doesn't use iframes or shadow DOM, so review the code examples above to understand the APIs. If your team's app uses iframes, apply
frameLocatorthe same way shown here.
Time: 35 min
Every action in Playwright auto-waits for the element to be visible, enabled, and stable before acting. You don't add manual waits.
-
click()— clicks an element:<button>Add to Cart</button>
await page.getByRole('button', { name: 'Add to Cart' }).click();
-
click({ force: true })— bypass visibility/enabled checks (use sparingly):await page.getByRole('button', { name: 'Submit' }).click({ force: true });
Normally Playwright refuses to click hidden or disabled elements.
force: trueoverrides that. Only use when you know what you're doing — e.g., a hidden file input or a button that becomes enabled via JS after some async load. -
fill()— clears an input field and types new text instantly (fastest, most reliable):<input type="text" placeholder="Username" />
await page.getByPlaceholder('Username').fill('standard_user');
-
pressSequentially()— presses each key one at a time, like a human typing, with optional delay:await page.getByPlaceholder('Username').pressSequentially('standard_user', { delay: 100 });
Use it when:
- The app listens for individual key events (e.g., auto-complete, OTP inputs, search suggestions)
- You need visual feedback for demos, videos, or slow-motion recordings
- Otherwise prefer
fill()— it's faster and more reliable
-
selectOption()— picks from a<select>dropdown:<select id="sort"> <option value="az">Name (A to Z)</option> <option value="za">Name (Z to A)</option> </select>
await page.getByLabel('Sort').selectOption('za'); // or by label text: await page.getByLabel('Sort').selectOption('Name (Z to A)');
-
check()/uncheck()— toggle checkboxes and radio buttons:<input type="checkbox" id="terms" /> <label for="terms">I agree</label>
await page.getByLabel('I agree').check(); await page.getByLabel('I agree').uncheck();
-
page.keyboard.press()— simulate keyboard keys:await page.keyboard.press('Enter'); // press Enter await page.keyboard.press('Tab'); // move focus await page.keyboard.press('Control+a'); // select all await page.keyboard.press('Escape'); // close modals
-
page.mouse— direct mouse actions (rare — preferclick()on locators):await page.mouse.click(100, 200); // click at pixel coordinates await page.mouse.dblclick(100, 200); // double-click await page.mouse.wheel(0, 500); // scroll down 500px
-
Demo: automate a login flow on saucedemo.com —
goto,fillusername,fillpassword,clicklogin button. Then add a dropdown select for sorting products.
Time: 45 min
-
Auto-waiting — Playwright commands don't act immediately. They wait for the element to be visible, enabled, and stable before proceeding. You never write
sleep(1000)orwaitForElement().// No manual waits needed — Playwright waits until the element is ready: await page.getByRole('button', { name: 'Login' }).click(); // If the button appears after 2 seconds, click() waits those 2 seconds automatically.
-
Always use web-first assertions — assertions built into Playwright's
expectthat auto-retry and wait for the condition. Never use raw JavaScript checks for web testing.❌ Without web-first (raw JS — fragile, no retry, bad errors):
const text = await page.locator('.message').textContent(); expect(text).toBe('Success'); // fails if element hasn't rendered yet const visible = await page.locator('.spinner').isVisible(); expect(visible).toBe(false); // fails if spinner is still showing
✅ With web-first (auto-retries, waits for condition, helpful error messages):
await expect(page.getByTestId('message')).toHaveText('Success'); await expect(page.getByTestId('spinner')).toBeHidden();
Benefits of web-first assertions:
- Auto-retry — they keep checking until the condition is met or timeout expires
- Readable errors —
"Expected element to be visible but was hidden"instead of"Expected true to be false" - No manual
awaiton locators — you pass the locator, not the resolved value - Consistent timeout — respects the global timeout in config, no magic numbers
-
Auto-retrying assertions —
expect(locator)doesn't check once. It retries until the assertion passes or the timeout expires.// This retries until the text appears or 5 seconds pass: await expect(page.getByText('Thank you')).toBeVisible(); // Equivalent to: keep checking every 500ms for up to 5 seconds
-
Common assertions with examples:
// Text content await expect(page.getByRole('heading')).toHaveText('Products'); await expect(page.getByTestId('cart-badge')).toContainText('3'); // Visibility await expect(page.getByText('Error: Password is required')).toBeVisible(); await expect(page.getByRole('dialog')).toBeHidden(); // Form values await expect(page.getByPlaceholder('Username')).toHaveValue(''); await expect(page.getByLabel('Email')).toHaveAttribute('type', 'email'); // Page-level await expect(page).toHaveURL(/.*inventory\.html/); // regex match await expect(page).toHaveTitle('Swag Labs');
-
Negative assertions — check something is NOT present:
await expect(page.getByText('Error')).not.toBeVisible(); await expect(page.locator('.spinner')).not.toBeVisible();
-
Soft assertions —
expect.soft()doesn't stop the test on failure. The test continues and reports all failures at the end:await expect.soft(page.getByRole('heading')).toHaveText('Products'); await expect.soft(page.getByTestId('cart-badge')).toContainText('3'); // If both fail, both are reported — test doesn't stop at the first one
-
Custom timeout — override the default per assertion:
await expect(page.getByText('Processing...')).toBeVisible({ timeout: 10000 });
-
Capturing failures — configure Playwright to automatically capture screenshots, videos, and traces when a test fails. Add this to
playwright.config.ts:export default defineConfig({ use: { screenshot: 'only-on-failure', // PNG of the page video: { mode: 'retain-on-failure', // video recording at 1920x1080 size: { width: 1920, height: 1080 }, }, trace: 'retain-on-failure', // full debug trace on any failure }, });
screenshot: 'only-on-failure'— saves a PNG of what the page looked like at the moment of failurevideowithmode: 'retain-on-failure'— records the full test run as a video, kept only if the test failstrace: 'retain-on-failure'— captures network requests, console logs, DOM snapshots, and timings (view withnpx playwright show-trace)- No
retriesset locally — if a test fails, you see the failure immediately without waiting for a retry
-
CI-aware configuration — when running in CI (GitHub Actions, Jenkins, etc.), you want stricter behavior and different settings than local runs. Use
process.env.CIto toggle:export default defineConfig({ forbidOnly: !!process.env.CI, // prevent `test.only` from being committed retries: process.env.CI ? 2 : 0, // retry only in CI (flaky tests) workers: process.env.CI ? 1 : undefined, // single worker in CI to reduce load use: { headless: process.env.CI ? true : false, // headed locally, headless in CI screenshot: 'only-on-failure', video: { mode: 'retain-on-failure', size: { width: 1920, height: 1080 }, }, trace: 'retain-on-failure', }, });
forbidOnly— if someone accidentally commits atest.only(), the CI run fails immediately instead of running just that one testworkers: 1— CI environments have limited resources; one worker prevents resource contentionheadless: falselocally — you can watch the browser do its thing during development; CI forces headless because there's no display
-
Demo: add assertions to the login flow on saucedemo.com — check error text is visible on failed login, check heading text after successful login, then use
expect.softto collect multiple checks without stopping early
Time: 25 min
Playwright provides hooks to run code before/after tests, and a convention for organizing test files.
-
File organization — one file per feature, one
describeper page/section, onetestper scenario:tests/ non-bdd/ login.spec.ts # describe('Login') — all login scenarios inventory.spec.ts # describe('Inventory') — all product listing scenarios -
test.describe()— groups related tests together. The output shows the group name as a heading.import { test, expect } from '@playwright/test'; test.describe('Login', () => { test('successful login', async ({ page }) => { await page.goto('https://www.saucedemo.com'); await page.locator('#user-name').fill('standard_user'); await page.locator('#password').fill('secret_sauce'); await page.locator('#login-button').click(); }); });
-
Hooks —
beforeEach(),afterEach(),beforeAll(),afterAll(). Useful for setup like clearing state or logging in, but keep tests explicit:test.afterEach(async ({ page }) => { await page.evaluate(() => localStorage.clear()); });
In practice, tests often call
page.goto()directly in each test. This keeps each test self-contained and easier to read — you don't have to look up whatbeforeEachdoes. -
Test data: variables over hardcoding — define data at the top so it's easy to change:
const STANDARD_USER = 'standard_user'; const PASSWORD = 'secret_sauce'; test('successful login', async ({ page }) => { await page.goto('https://www.saucedemo.com'); await page.locator('#user-name').fill(STANDARD_USER); await page.locator('#password').fill(PASSWORD); await page.locator('#login-button').click(); });
-
Create a project for your tests — recall in Lesson 4 you removed Firefox and WebKit and kept only Chromium. Now we'll rename the project and point it to your test folder. Open
playwright.config.tsand update the existingprojectsblock:// Before: projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], // After: projects: [ { name: 'non-bdd-tests', testDir: 'tests/non-bdd', use: { browserName: 'chromium', viewport: { width: 1920, height: 1080 }, }, }, ],
By defining projects with explicit names and paths, you control exactly which tests run. Your single test now runs once under the
non-bdd-testsproject.Run only these tests:
npx playwright test --project non-bdd-tests -
Demo: create
tests/non-bdd/login.spec.tswith 3 tests in adescribe('Login')block — one successful login, one locked-out user, one failed login with empty credentials. Use variables for credentials, CSS selectors for locators.
Time: 60 min
This is where test automation gets professional. The Page Object Model (POM) is the most widely used design pattern in UI testing.
-
The problem — without POM, tests mix locators, actions, and assertions together. Every test repeats the same selectors. When the UI changes, you hunt through every file to update selectors.
❌ Messy test without POM:
test('login then check inventory', async ({ page }) => { await page.goto('https://www.saucedemo.com'); await page.locator('#user-name').fill('standard_user'); await page.locator('#password').fill('secret_sauce'); await page.locator('#login-button').click(); await expect(page.locator('.title')).toHaveText('Products'); });
Now imagine 50 tests like this. If the login button's
idchanges, you fix it 50 times. -
The solution — a Page Object class encapsulates everything about one page: its locators and the actions you can perform on it.
// src/pages/BasePage.ts import { Page } from '@playwright/test'; export class BasePage { protected readonly page: Page; constructor(page: Page) { this.page = page; } async goto(url: string) { await this.page.goto(url); } }
// src/pages/LoginPage.ts import { Page, Locator } from '@playwright/test'; import { BasePage } from './BasePage'; export class LoginPage extends BasePage { constructor(page: Page) { super(page); } private get usernameInput(): Locator { return this.page.locator('#user-name'); } private get passwordInput(): Locator { return this.page.locator('#password'); } private get loginButton(): Locator { return this.page.locator('#login-button'); } private get errorMessage(): Locator { return this.page.locator('[data-test="error"]'); } async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.loginButton.click(); } async getErrorMessage(): Promise<string> { return (await this.errorMessage.textContent()) ?? ''; } }
-
Now the test is clean — one line to do what used to be five:
const loginPage = new LoginPage(page); await loginPage.goto('https://www.saucedemo.com'); await loginPage.login('standard_user', 'secret_sauce'); const error = await loginPage.getErrorMessage();
-
Access modifiers on getter methods — locator getters are
privatebecause tests shouldn't reach into the page object and manipulate elements directly. Instead, tests use the page's public methods (login(),getErrorMessage()). This enforces encapsulation: the page is the single source of truth for how to interact with the UI. If a selector changes, you update one file — no test needs to know.-
private— locator used only within the page (most locators). Tests interact via public methods. -
public— locator you intentionally expose for tests to use when no wrapper method exists. -
protected— locator shared among related pages via inheritance (rare in POM).Modifier Accessible in class Accessible in subclass Accessible from test private✅ ❌ ❌ protected✅ ✅ ❌ public✅ ✅ ✅
-
-
The InventoryPage — after login, you land on the inventory page. Create a page object for it too:
// src/pages/InventoryPage.ts import { Page, Locator, expect } from '@playwright/test'; import { BasePage } from './BasePage'; export class InventoryPage extends BasePage { constructor(page: Page) { super(page); } private get title(): Locator { return this.page.locator('.title'); } async expectOnPage() { await expect(this.title).toHaveText('Products'); } }
expectOnPage()is a page-level guard — it verifies you're on the right page by checking a unique heading. Assertions inside page objects are acceptable for this purpose (navigational verification). -
Putting it together — the messy example becomes:
const loginPage = new LoginPage(page); await loginPage.goto('https://www.saucedemo.com'); await loginPage.login('standard_user', 'secret_sauce'); const inventoryPage = new InventoryPage(page); await inventoryPage.expectOnPage();
Time: 25 min
-
What is a fixture? — a reusable piece of test setup. Playwright gives you built-in ones (
page,browser,context). Custom fixtures let you add your own. -
Why fixtures over manual setup in tests?
- Lazy — a fixture is only created when a test actually requests it. If 3 out of 10 tests need
loggedInPage, only those 3 pay the cost. - Composable — fixtures can depend on other fixtures (e.g.,
loginPagedepends onpage) - Auto-cleanup — whatever you set up in a fixture, you tear down with
use()and code after it - Type-safe — fixtures are typed, so your IDE knows what's available
- Lazy — a fixture is only created when a test actually requests it. If 3 out of 10 tests need
-
Page Object fixtures — create a fixture for each page class so tests just ask for the page they need:
// src/fixtures/fixtures.ts import { test as base } from 'playwright-bdd'; import { LoginPage } from '../pages/LoginPage'; import { InventoryPage } from '../pages/InventoryPage'; type MyFixtures = { loginPage: LoginPage; inventoryPage: InventoryPage; }; export const test = base.extend<MyFixtures>({ loginPage: async ({ page }, use) => { // Do some pre-condition/setup steps here... await use(new LoginPage(page)); // Do some data-cleanup/teardown steps here... }, inventoryPage: async ({ page }, use) => { // Do some pre-condition/setup steps here... await use(new InventoryPage(page)); // Do some data-cleanup/teardown steps here... }, });
BDD step definitions import this
testso all fixtures are available in Gherkin steps:// src/steps/loginSteps.ts import { test } from '../fixtures/fixtures'; import { expect } from '@playwright/test';
⚠️ Critical: when using custom fixtures, importtestfrom your fixture file (../fixtures/fixtures), not from@playwright/test. Thetestfrom@playwright/testdoesn't know about yourloginPageorinventoryPagefixtures — TypeScript will error, and if you ignore the error, the fixtures won't be injected at runtime. The only exception is non-BDD tests that don't use custom fixtures at all (they create page objects manually withnew LoginPage(page)). -
Key insight — lazy by nature: fixtures are only instantiated when a test lists them as a parameter. A test that only needs
loginPagewon't createinventoryPage. This keeps tests fast — you don't pay for what you don't use.// inventoryPage fixture is never built here: test('login error', async ({ loginPage }) => { ... }); // Both are built here: test('full flow', async ({ loginPage, inventoryPage }) => { ... });
-
How it differs from
beforeEach: fixtures are type-safe (IDE knows what's available), composable (can depend on each other), lazy (only created when requested), and have a clean setup/teardown pattern.beforeEachis simpler for one-off setup but doesn't scale as well across many test files.
Time: 20 min
Install: npm i -D dotenv
-
Why environment variables? — credentials, URLs, and config values change between environments (local, dev, staging, CI). Hardcoding them means editing code for every environment. Env vars keep config out of code.
-
What is dotenv? — a library that reads a
.envfile and loads each line intoprocess.env. Now your code reads fromprocess.env.BASE_URLinstead of a hardcoded string. -
Step-by-step:
-
Create
.envin the project root:BASE_URL="https://www.saucedemo.com" STANDARD_USER="standard_user" LOCKED_OUT_USER="locked_out_user" PASSWORD="secret_sauce"
The quotes ensure the entire value is captured, including any special characters.
.envcontains real values. Add it to.gitignore— never commit secrets. -
Create
.env.exampleas a template (keys only, no values) and commit it:BASE_URL="" STANDARD_USER="" LOCKED_OUT_USER="" PASSWORD=""
This serves as documentation for which environment variables are needed. New developers copy it to
.env, fill in the values, and the project runs. Since.env.exampleis committed to GitHub, never put real credentials in it — even if they're publicly known, it's a bad habit that leads to secrets leaking in real projects. -
Load dotenv at the top of
playwright.config.ts:import dotenv from 'dotenv'; dotenv.config({ path: '.env' });
The
{ path: '.env' }is explicit so there's no ambiguity about which file to load.
-
-
Using env vars in tests — access them via
process.env:// Without env vars: await loginPage.login('standard_user', 'secret_sauce'); // With env vars: await loginPage.login(process.env.STANDARD_USER ?? 'standard_user', process.env.PASSWORD ?? 'secret_sauce');
-
Why
??fallbacks? — the??operator provides a default if the env var is undefined. This means:- Tests run out of the box without a
.envfile (useful for quick demos or CI scaffolding) - CI/CD can override by setting real environment variables natively (they take precedence over
.env) - You don't get cryptic
undefinederrors when someone forgets to create.env
- Tests run out of the box without a
-
Using env vars in config — set
baseURLfrom the env so tests use relative URLs. It goes in thenon-bdd-testsproject'suseblock (the BDD project gets URLs from feature file examples):// playwright.config.ts use: { baseURL: process.env.BASE_URL, },
The
.envfile provides the value. If.envis missing,baseURLwill beundefinedand Playwright will throw a clear error — this is intentional so you don't accidentally run against the wrong environment. -
Demo: create
.envwith the saucedemo credentials, load it in config, update the login test to useprocess.env.STANDARD_USERandprocess.env.PASSWORDwith??fallbacks. Then run with and without.envto confirm it works both ways.
Time: 65 min
Install: npm i -D playwright-bdd
-
What is BDD? — Behavior-Driven Development. You write tests in plain English sentences that describe what the app should do, not how the code works. Non-technical stakeholders (PMs, QA, business analysts) can read and even write these scenarios.
-
What is Gherkin? — the plain-English syntax BDD uses. Three keywords:
Given <some initial state> When <some action> Then <some expected outcome> -
Why playwright-bdd instead of Cucumber? — the traditional Cucumber library creates its own test runner, separate from Playwright. This means you lose Playwright's built-in features: auto-waiting, fixtures, tracing, video capture, HTML reporter, and project config. playwright-bdd is a lightweight alternative that compiles Gherkin feature files into Playwright test specs, so everything runs directly on the Playwright runner — no features lost.
In short: you get the readability of BDD without sacrificing any of Playwright's power.
-
Install the Cucumber VS Code extension — the official one by "OpenCucumber" in the VS Code marketplace. This gives you Gherkin syntax highlighting, auto-complete for step definitions, and click-to-navigate between
.featurefiles and step code.Then configure
.vscode/settings.jsonso the extension knows where to find your features and steps:{ "cucumber.features": ["src/features/*.feature"], "cucumber.glue": ["src/steps/**/*.ts"], "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" } -
Write a feature file — place it in
src/features/:# src/features/LoginTest.feature @login @bdd-tests Feature: User Login @smoke @core Scenario Outline: <ID> Successful Login - Normal User Given user launch url "<URL>" When user login using username "<USERNAME>" and password "<PASSWORD>" Then user should land on Home page Examples: | ID | URL | USERNAME | PASSWORD | | 1 | https://www.saucedemo.com/ | standard_user | secret_sauce |
@login @bdd-tests— feature-level tags.@bdd-testsis used by the BDD project config to select these tests.Scenario Outline— a parameterized scenario. The placeholders in angle brackets (<URL>,<USERNAME>, etc.) are filled from theExamples:table.- Each row in
Examples:produces one test case. Add more rows to test multiple combinations without rewriting the steps. - Deliberate failure — for demo purposes, include scenarios that fail, e.g., checking an error message that doesn't match:
@sanity @extended Scenario Outline: <ID> Unsuccessful Login - Empty Credentials Given user launch url "<URL>" When user login using username "<USERNAME>" and password "<PASSWORD>" Then user should see an error message "<ERROR_MESSAGE>" # id(1) is deliberate failure to demonstrate screenshot/video capture on failure Examples: | ID | URL | USERNAME | PASSWORD | ERROR_MESSAGE | | 1 | https://www.saucedemo.com/ | abc123 | | Username is required | | 2 | https://www.saucedemo.com/ | | secret_sauce | Username is required |
This also shows how
Scenario Outlinesupports different column counts per table — the third scenario addsERROR_MESSAGEandThen user should see an error messageto validate it. -
Configure playwright-bdd — before you can generate step files, add
defineBddConfigtoplaywright.config.ts. This tells playwright-bdd where your.featurefiles are and where to output the generated specs:import { defineBddConfig } from 'playwright-bdd'; import { defineConfig } from '@playwright/test'; const bddTestDir = defineBddConfig({ paths: ['src/features/**/*.feature'], require: ['src/steps/**/*.ts', 'src/fixtures/**/*.ts'], });
Then add a
bdd-testsproject alongside your existingnon-bdd-testsproject:export default defineConfig({ projects: [ { name: 'non-bdd-tests', testDir: 'tests/non-bdd', use: { browserName: 'chromium', viewport: { width: 1920, height: 1080 }, }, }, { name: 'bdd-tests', testDir: bddTestDir, use: { browserName: 'chromium', viewport: { width: 1920, height: 1080 }, }, }, ], });
defineBddConfigreturns a path to the auto-generated.features-gen/folder — thebdd-testsproject uses it as itstestDir, so Playwright picks up the compiled spec files. -
Generate step stubs — now that the config is set up, run:
npx bddgen
playwright-bdd reads your
.featurefiles and generatessrc/steps/loginSteps.tswith empty function bodies for each step. -
Implement step definitions — fill in the generated stubs, calling your existing Page Object Model. Playwright-bdd step definitions use
{string}for quoted parameters from the Gherkin:// src/steps/loginSteps.ts import { createBdd } from 'playwright-bdd'; import { expect } from '@playwright/test'; import { DataTable } from 'playwright-bdd'; import { test } from '../fixtures/fixtures'; const { Given, When, Then } = createBdd(test); Given('user launch url {string}', async ({ loginPage }, url: string) => { await loginPage.goto(url); }); When( 'user login using username {string} and password {string}', async ({ loginPage }, username: string, password: string) => { await loginPage.login(username, password); }, ); Then('user should land on Home page', async ({ inventoryPage }) => { await inventoryPage.expectOnPage(); }); Then('user should see an error message {string}', async ({ loginPage }, expectedMessage: string) => { const actualMessage = await loginPage.getErrorMessage(); expect(actualMessage).toContain(expectedMessage); });
{string}in the step name matches a quoted value"..."from the feature file and passes it as a parameter- Fixtures from
../fixtures/fixtures(e.g.,loginPage,inventoryPage) are destructured in the first argument — these are the same Page Objects from Lesson 11 - DataTable — for tabular data in Gherkin (like the accepted usernames list), use Cucumber's
DataTabletype:
Then user should see all the accepted usernames | USERNAMES | | standard_user | | locked_out_user | | problem_user | | performance_glitch_user |
Then('user should see all the accepted usernames', async ({ loginPage }, dataTable: DataTable) => { const expectedUsernames = dataTable.hashes().map((row) => row.USERNAMES); const actualUsernames = await loginPage.getAcceptedUsernames(); for (const username of expectedUsernames) { expect(actualUsernames).toContain(username); } });
The step definitions use the same page objects your non-BDD tests use — zero duplication.
-
Run BDD tests — now that the
bdd-testsproject is configured, run with either the project name or tag:npx playwright test --project bdd-tests npx playwright test --grep "@bdd-tests"
-
Why use BDD?
- Non-technical people can write or review test scenarios
- Feature files become living documentation — always in sync with what's tested
- Encourages teams to agree on behavior before writing code
-
Why not always? — BDD adds boilerplate (feature files + step definitions). For small teams or simple apps, plain Playwright tests with POM are faster to write and maintain. BDD shines when multiple roles need to collaborate on tests.
-
Demo: write a
.featurefile with aScenario Outline(successful login + locked out user), runnpx bddgen, implement the step definitions usingLoginPageandInventoryPage, and run both scenarios with--project bdd-tests.
Time: 25 min
Install: npm i -D allure-playwright allure-commandline
-
So far you've been using Playwright's default list reporter (terminal output) and HTML reporter (browsable report). They're built into Playwright — no extra install needed. Allure is a third option that gives you richer, interactive reports with history, graphs, and failure analysis.
-
What is Allure? (an open-source reporting framework that produces detailed HTML reports with timelines, categories, graphs, and test history)
-
Add the Allure reporter alongside your existing reporters in
playwright.config.ts:reporter: [['list'], ['html'], ['allure-playwright', { resultsDir: 'allure-results' }]];
The
resultsDiris where Allure stores raw data. This folder is consumed by the Allure CLI to generate the interactive report. -
Add an npm script to serve the report:
"allure:serve": "npx allure serve allure-results"
-
Run tests, then
npm run allure:serve— this opens a browser with the Allure dashboard -
What you see: timeline view, categories (passed/failed/broken), graphs, and per-test details including screenshots, videos, and traces captured on failure
-
See a live demo — check out Allure 3 demo reports to see what the final report looks like before you run your own tests
Time: 15 min
In Lesson 14 you added the bdd-tests project alongside non-bdd-tests. Both projects share the same browser and viewport settings, but right now they're duplicated. Let's clean that up.
-
Extract shared browser config — pull the common
usesettings into abrowserConfigconst so both projects reference the same object:import { defineBddConfig } from 'playwright-bdd'; import { defineConfig, devices } from '@playwright/test'; const bddTestDir = defineBddConfig({ paths: ['src/features/**/*.feature'], require: ['src/steps/**/*.ts', 'src/fixtures/**/*.ts'], }); const browserConfig = { ...devices['Desktop Chrome'], viewport: { width: 1920, height: 1080 }, } as const; export default defineConfig({ projects: [ { name: 'non-bdd-tests', testDir: 'tests/non-bdd', use: { ...browserConfig, baseURL: process.env.BASE_URL, }, }, { name: 'bdd-tests', testDir: bddTestDir, use: browserConfig, }, ], });
browserConfigis shared between both projects via the spread operator (...browserConfig)- The
non-bdd-testsproject addsbaseURLon top (it needs relative URLs forpage.goto('/')) - The
bdd-testsproject gets its URLs from the feature file'sExamples:table, so it doesn't needbaseURL as constlocks the object so TypeScript infers literal types (helps with autocomplete)
-
Run projects independently:
npx playwright test # runs all projects npx playwright test --project non-bdd-tests # non-BDD only npx playwright test --project bdd-tests # BDD only
-
Auto-regenerate BDD specs — every time you edit a
.featurefile, you need to re-runnpx bddgen. Instead of apretestscript (which would run before every test, even non-BDD), addbddgenonly to the scripts that need it:"test": "npx bddgen && npx playwright test", "test:bdd": "npx bddgen && npx playwright test --project=bdd-tests", "test:non-bdd": "npx playwright test --project=non-bdd-tests",
npm test/npm run test:bdd— runsnpx bddgenfirst (feature files → Playwright specs), then the testsnpm run test:non-bdd— skipsbddgenentirely (no feature files to compile), runs directly
Time: 30 min
-
Review the final project structure:
├── src/ │ ├── features/ # Gherkin .feature files (BDD scenarios) │ ├── steps/ # Step definitions mapped to feature files │ ├── pages/ # Page Object Model classes │ └── fixtures/ # Custom fixtures with typed test objects ├── tests/ │ └── non-bdd/ # Non-BDD test specs using POM directly ├── .vscode/ │ └── settings.json # VS Code config — Cucumber extension, Prettier ├── .features-gen/ # Auto-generated BDD specs (do not edit) ├── playwright.config.ts # Central config — projects, reporters, baseURL, timeouts ├── tsconfig.json # TypeScript compiler options ├── .prettierrc # Prettier formatting rules ├── .env # Local env vars (gitignored) ├── .env.example # Template env vars (committed) ├── .gitignore # Files/folders Git should ignore └── package.json # Scripts, dependencies -
Package scripts — put common commands in
package.jsonso the team runs the same things the same way:{ "scripts": { "setup": "npm install && npx playwright install", "setup:info": "echo 'Installs dependencies and downloads Playwright browsers'", "test": "npx bddgen && npx playwright test", "test:info": "echo 'Runs all suites (BDD + non-BDD)'", "test:bdd": "npx bddgen && npx playwright test --project bdd-tests", "test:bdd:info": "echo 'Runs BDD tests only'", "test:non-bdd": "npx playwright test --project non-bdd-tests", "test:non-bdd:info": "echo 'Runs non-BDD tests only'", "report": "npx playwright show-report", "report:info": "echo 'Opens Playwright HTML report'", "allure:serve": "npx allure serve allure-results", "allure:serve:info": "echo 'Opens Allure report in browser'" } }npm run setup— installs dependencies and downloads browsers (run once when cloning)npm test— runs everything (BDD + non-BDD)npm run test:bdd/test:non-bdd— runs one suite at a time during development- Append
:infoto any script name to see what it does without running it
-
CI vs Local — how env vars work:
The
.envfile is only for local development. In CI, you set environment variables natively in the CI pipeline UI or config (e.g., GitHub Actions secrets). Both feed intoprocess.env:baseURL: process.env.BASE_URL,
- Locally: create
.envwithBASE_URL="https://www.saucedemo.com"— Playwright reads it viadotenv. - In CI: set
BASE_URL,STANDARD_USER,PASSWORDas native environment variables in the CI platform — no.envfile needed. - Result: the same config file works in both environments. CI credentials stay secure in the pipeline's secret store, never written to a file.
- Locally: create
-
Prettier — consistent code formatting — add a
.prettierrcfile to keep everyone's code formatted the same way:{ "printWidth": 120, "singleQuote": true, "trailingComma": "all", "tabWidth": 2, "semi": true, "arrowParens": "always", "endOfLine": "lf" }Install the Prettier VS Code extension (
esbenp.prettier-vscode) and configure VS Code to format on save via the final.vscode/settings.json:{ "cucumber.features": ["src/features/**/*.feature"], "cucumber.glue": ["src/steps/**/*.ts"], "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "[feature]": { "editor.defaultFormatter": "CucumberOpen.cucumber-official" }, "[cucumber]": { "editor.defaultFormatter": "CucumberOpen.cucumber-official" } }editor.formatOnSave: true— auto-formats every file you saveeditor.defaultFormatter— uses Prettier for all files[feature]and[cucumber]— override to use the Cucumber extension's own formatter for Gherkin files (Prettier doesn't format.featurefiles)
-
Best practices recap:
- Locators: prefer
getByRole,getByLabel,getByTextover CSS/XPath — they auto-wait, are accessibility-first, and break less on DOM changes - POM: one class per page, readable locators, public action methods, page-level guards for navigation verification
- Fixtures: keep setup reusable and lazy — only create what each test asks for
- Env vars:
.envfor local dev, native env vars for CI, never commit secrets - CI-aware config:
forbidOnly, conditionalretries/workers/headlessviaprocess.env.CI - Tags:
@bdd-testsand@non-bdd-testson every test so the HTML report tag filter works - Deliberate failures: one per suite to demonstrate failure capture (screenshots, videos, traces)
- Independence: tests should not depend on each other — no shared state, no relying on a previous test's side effects
- Config: set timeouts, retries, and failure capture in config — not in test code
- Formatting: Prettier with
.prettierrc+editor.formatOnSave: truefor consistent code style
- Locators: prefer
-
How to add a new feature: create a page class → write tests (BDD or non-BDD) → add env vars if needed → run with
--project -
Add-on: README.md for GitHub — when you're ready to share your project (GitHub, GitLab, etc.), add a
README.mdat the root. It introduces others to what your project is and how to use it. A minimal template:# Playwright UI Testing Framework UI test automation using Playwright with BDD (Gherkin) and Page Object Model. ## Prerequisites - Node.js 20+ - npm ## Setup ```bash npm run setup # installs deps + downloads browsers cp .env.example .env # configure environment variables
Command What it runs npm testAll suites (BDD + non-BDD) npm run test:bddBDD tests only npm run test:non-bddNon-BDD tests only npm run reportOpen Playwright HTML report npm run allure:serveOpen Allure report src/ pages/ # Page Object Model classes features/ # Gherkin .feature files steps/ # Step definitions fixtures/ # Custom Playwright fixtures tests/ non-bdd/ # Non-BDD test specs- Playwright — test runner and browser automation
- playwright-bdd — BDD/Gherkin support
- Allure — test reporting
- dotenv — environment variable management
- Prettier — code formatting
Keep it short — just enough for someone to clone and run. Update the sections as your project grows.
Time: 10 min
By now you have multiple ways to run tests depending on what you're working on.
-
Run everything (BDD + non-BDD):
npm testThis runs
npx bddgento compile feature files, thennpx playwright testto run all projects. -
Run only non-BDD tests:
npm run test:non-bdd
-
Run only BDD tests:
npm run test:bdd
Use these during development — faster feedback when you're working on one suite.
-
Open Playwright's HTML report:
npm run report
Shows pass/fail, test duration, and error details from the last run.
-
Open Allure report:
npm run allure:serve
Launches the interactive Allure dashboard with graphs, history, timelines, and failure screenshots/videos.
-
Quick reference:
Command What it does npm testRun all suites npm run test:bddRun BDD tests only npm run test:non-bddRun non-BDD tests only npm run reportOpen Playwright HTML report npm run allure:serveOpen Allure report