Build a Playwright E2E Test Portfolio
Build a professional QA automation portfolio with Playwright and CI/CD.
Introduction
30 Second Summary
Every QA engineer knows the frustration of telling hiring managers "I can automate tests" without proof they can run themselves. A portfolio that actually executes on demand speaks louder than any resume bullet point.
In this project, you will build a professional end-to-end test suite using Playwright and TypeScript, deploy it to GitHub with a CI pipeline, and demonstrate the exact automation skills hiring managers look for.
What You'll Build
A Playwright test suite that automatically tests a real e-commerce web application, organized with the Page Object Model pattern and run through GitHub Actions CI.
By the end of this project, you'll have:
- A working Playwright test suite with login and product test cases running against a real web application.
- A Page Object Model architecture that demonstrates professional code organization to hiring managers.
- A GitHub Actions CI pipeline that auto-runs your tests on every push and publishes an HTML report.
- Secret Mission: Add a full checkout flow test that chains multiple page objects together.
Are there any prerequisites?
You need Node.js, VS Code, and a GitHub account. This guide walks you through installing everything else, including Playwright and its browsers.
Set Up Your Playwright Project
Every QA automation portfolio starts with the same foundation: a well-structured test project connected to version control. Playwright is the industry-standard framework for end-to-end testing, and pairing it with GitHub gives hiring managers a live, runnable proof of your skills.
In this step, you will scaffold a Playwright project with TypeScript, configure it to test a real web application, and push the whole thing to a public GitHub repository.
In this step, get ready to:
- Create a Playwright project with TypeScript and browsers installed.
- Configure the project to target SauceDemo with a single browser for fast local development.
- Push your project to a public GitHub repository.
Create a new directory and initialize Playwright
Playwright needs its own project folder with a package.json and configuration file. The npm init playwright@latest command scaffolds all of this for you in one go.
- Open your terminal inside VS Code by pressing Ctrl+` (macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir playwright-portfolio
cd playwright-portfolio
- Confirm the folder was created by running:
ls
The folder will be empty for now. That is expected.
- Initialize Playwright by running this command:
npm init playwright@latest
What prompts will I see?
The setup wizard asks four questions. Select or confirm each one:
- TypeScript or JavaScript: select TypeScript (the default).
- Tests folder name: press Enter to accept the default tests folder.
- Add a GitHub Actions workflow: select true (yes).
- Install Playwright browsers: press Enter to confirm (default: yes).
Browser installation may take a minute or two. Wait for the command to finish completely before continuing.
- Verify the scaffold was created by running:
ls
You should see playwright.config.ts, package.json, and a tests folder in the output.
- Run the example test to confirm everything works:
npx playwright test
You should see output indicating that tests passed. The exact number depends on the scaffold, but you should see green text confirming success.
Tests failing or command not found?
- If you see a permission error, make sure you ran the command from inside the playwright-portfolio folder.
- If browsers failed to install, run npx playwright install --with-deps to retry.
- If npx is not recognized, confirm Node.js is installed by running node --version in your terminal.
Help me troubleshoot my Playwright installation. I ran npm init playwright@latest but tests are not passing.
Clean up the scaffold
The scaffold includes an example test that is not part of your portfolio. You also need to point the config at the application you will be testing.
- Delete the example test file by running:
rm tests/example.spec.ts
The tests folder is now empty, ready for the test files you will write in the next steps.
- Open the playwright-portfolio folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Select the playwright-portfolio folder on your Desktop, then click Open.
- In the Explorer sidebar, click playwright.config.ts to open it.
- Select all the existing content and replace it with the following code:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'https://www.saucedemo.com',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
What does this config do?
- baseURL: 'https://www.saucedemo.com' tells Playwright that every test navigating to / will go to the SauceDemo site.
- The projects array now lists only Chromium. This keeps local test runs fast while you develop.
- reporter: 'html' generates an HTML report after every run, which becomes a CI artifact later.
- Save playwright.config.ts by pressing Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Double check that you have saved playwright.config.ts before moving on.
ⓧ I'd like to double check the full code
Here is the complete playwright.config.ts file. Make sure yours matches exactly:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'https://www.saucedemo.com',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
Create a GitHub repository
Your project needs a public GitHub repository so hiring managers can browse your test code and see CI results. You will create the repo on GitHub first, then push your local project to it.
- Open your browser and go to github.com/new.
- Enter playwright-portfolio in the Repository name field.
- Select Public for visibility.
- Leave all other options unchecked (no README, no .gitignore, no license).
- Click Create repository.
GitHub shows a quick setup page with commands. You will use those commands from your terminal.
- Switch back to your VS Code terminal and initialize git, then push your project by running these commands:
git init
git add .
git commit -m "Initial Playwright setup"
git branch -M main
git remote add origin https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/playwright-portfolio.git
git push -u origin main
What do these commands do?
- git init turns the folder into a Git repository.
- git add . stages all your project files.
- git commit saves a snapshot with the message "Initial Playwright setup".
- git remote add origin links your local repo to the one you just created on GitHub.
- git push uploads everything to GitHub.
Push rejected or authentication error?
- If you see a permission or authentication error, make sure you are signed into GitHub in your browser. Git may open a browser window asking you to authorize.
- If the remote URL is wrong, remove it with git remote remove origin and re-add it with the correct username.
- If you see "fatal: not a git repository", make sure your terminal is inside the playwright-portfolio folder.
Help me push my Playwright project to GitHub. I am getting an error when running git push.
- Refresh your GitHub repository page in the browser.
You should see your project files listed, including playwright.config.ts, package.json, and the .github/workflows folder.
Your Playwright project is now live on GitHub with a clean config pointing at SauceDemo. Next up, you will write your first login tests and see Playwright automate a real browser.
Write Your First Login Tests
Your project is scaffolded and connected to GitHub. Now it's time to write actual tests that prove you can automate real user interactions.
In this step, you'll test a login page by writing three scenarios: a happy path (valid credentials), a locked-out user, and invalid credentials. These cover positive and negative testing, which is exactly what hiring managers look for in a QA portfolio.
In this step, get ready to:
- Explore the application under test and identify element selectors.
- Write three login test cases covering success and failure scenarios.
- Verify all tests pass and commit your work.
Explore the application under test
Before you write any test code, you need to understand the application you're testing. You'll inspect the page to find the element selectors that your tests will interact with.
- Open https://www.saucedemo.com/ in your browser.
- Open DevTools by pressing Cmd+Option+I (macOS) or F12 (Windows).
- Click the element selector tool (the cursor icon in the top-left of DevTools).
- Click the username input field on the page.
In the Elements panel, you'll see the input element has a data-test attribute. This is the selector you'll use in your tests.
Why use data-test attributes?
Test automation best practice is to target dedicated test attributes like data-test instead of CSS classes or IDs. Classes change when designers update styles. Test attributes are stable because developers add them specifically for automation.
- Inspect the password field and the login button the same way. Note their data-test values: username, password, and login-button.
- Look at the credentials displayed on the login page. You'll use standard_user and locked_out_user in your tests, both with the password secret_sauce.
What is SauceDemo?
SauceDemo is a purpose-built e-commerce demo application created by Sauce Labs specifically for test automation practice. It has multiple test users designed to trigger different scenarios (successful login, locked account, performance issues).
It's stable, publicly accessible, and has data-test attributes on all its elements. This makes it ideal for building a portfolio.
Write login test cases
Now that you know the selectors and credentials, you'll write three test cases that cover a successful login, a locked-out user, and invalid credentials.
- Create a new file called login.spec.ts inside the tests folder of your playwright-portfolio project.
- Add the test structure and your first test (successful login) by pasting the following code into tests/login.spec.ts:
import { test, expect } from '@playwright/test';
test.describe('Login Page', () => {
test.beforeEach(async ({ page }) => {
// Navigate to SauceDemo before every test
await page.goto('/');
});
test('successful login with standard user', async ({ page }) => {
// Fill in valid credentials
await page.locator('[data-test="username"]').fill('standard_user');
await page.locator('[data-test="password"]').fill('secret_sauce');
await page.locator('[data-test="login-button"]').click();
// Verify the URL changes to the inventory page
await expect(page).toHaveURL(/inventory/);
});
});
What does this code do?
- import { test, expect } from '@playwright/test' brings in Playwright's test runner and assertion library.
- test.describe groups related tests under a label. This makes your HTML report organized.
- test.beforeEach runs before every individual test. Here it navigates to the login page so each test starts fresh.
- page.goto('/') uses the baseURL you configured in playwright.config.ts, so '/' resolves to https://www.saucedemo.com/.
- expect(page).toHaveURL(/inventory/) asserts the page navigated to the products page after a successful login.
- Save tests/login.spec.ts.
- Run your first test in headed mode (so you can watch the browser) by running this command in your terminal:
npx playwright test --headed
A Chromium browser window opens, fills in the credentials, clicks login, and closes. Your terminal shows 1 test passed.
Test failing or browser not opening?
- Make sure your file is saved and is named exactly login.spec.ts (not .test.ts). Playwright's config looks for .spec.ts files by default.
- Check that the file is inside the tests/ folder, not the project root.
- If you see a timeout error, check your internet connection. The test needs to reach saucedemo.com.
Help me debug why my Playwright test is failing
- Now add the two negative test cases. In tests/login.spec.ts, add the following two tests inside the test.describe block, below the closing }); of the first test:
test('locked out user cannot login', async ({ page }) => {
// Attempt login with a locked-out account
await page.locator('[data-test="username"]').fill('locked_out_user');
await page.locator('[data-test="password"]').fill('secret_sauce');
await page.locator('[data-test="login-button"]').click();
// Verify the error message mentions the lockout
await expect(page.locator('[data-test="error"]')).toContainText('locked out');
});
test('login fails with invalid credentials', async ({ page }) => {
// Attempt login with non-existent credentials
await page.locator('[data-test="username"]').fill('invalid_user');
await page.locator('[data-test="password"]').fill('wrong_password');
await page.locator('[data-test="login-button"]').click();
// Verify an error message appears
await expect(page.locator('[data-test="error"]')).toBeVisible();
});
What do these tests check?
- The locked-out test verifies that locked_out_user sees an error containing the text "locked out". This is a negative test that confirms the app handles account restrictions correctly.
- The invalid credentials test checks that a non-existent user sees a visible error. Using .toBeVisible() instead of checking specific text makes it resilient to wording changes.
- Both tests target the [data-test="error"] element, which SauceDemo displays when login fails.
- Save tests/login.spec.ts.
- Run all tests in headed mode again to watch all three execute:
npx playwright test --headed
You should see the browser open three times (once per test), and your terminal shows 3 tests passed.
One or more tests failing?
- Make sure the two new tests are placed inside the test.describe block's curly braces, not outside them.
- Check that every string uses matching quotes and every parenthesis is closed. A single missing ) or } breaks the whole file.
- Double check your code against the full file below.
Help me fix my failing Playwright login tests
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import { test, expect } from '@playwright/test';
test.describe('Login Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('successful login with standard user', async ({ page }) => {
await page.locator('[data-test="username"]').fill('standard_user');
await page.locator('[data-test="password"]').fill('secret_sauce');
await page.locator('[data-test="login-button"]').click();
await expect(page).toHaveURL(/inventory/);
});
test('locked out user cannot login', async ({ page }) => {
await page.locator('[data-test="username"]').fill('locked_out_user');
await page.locator('[data-test="password"]').fill('secret_sauce');
await page.locator('[data-test="login-button"]').click();
await expect(page.locator('[data-test="error"]')).toContainText('locked out');
});
test('login fails with invalid credentials', async ({ page }) => {
await page.locator('[data-test="username"]').fill('invalid_user');
await page.locator('[data-test="password"]').fill('wrong_password');
await page.locator('[data-test="login-button"]').click();
await expect(page.locator('[data-test="error"]')).toBeVisible();
});
});
Verify and commit
- Run your full test suite (without the browser window) to confirm everything passes cleanly:
npx playwright test
Your terminal should show 3 tests passed across 1 file.
- View the HTML report that Playwright generated by running:
npx playwright show-report
A browser tab opens showing a professional test report with your three test cases listed under "Login Page". You can click into each test to see timing details.
Why does the HTML report matter?
The HTML report is what your GitHub Actions workflow uploads as an artifact on every CI run. Anyone reviewing your portfolio can download it and see your test results presented professionally, without needing to run the tests themselves.
- Commit your new test file and push to GitHub by running these commands:
git add .
git commit -m "Add login page tests"
git push
Push rejected or authentication error?
- If git asks for credentials, make sure you're authenticated with GitHub via HTTPS token or SSH key.
- If the push is rejected because of a non-fast-forward error, run git pull --rebase first, then push again.
Help me push my Playwright project to GitHub
You now have three working tests that cover real login scenarios. Next up, you'll refactor these tests using the Page Object Model pattern to make your code cleaner and more maintainable.
Refactor with Page Object Model
Your login tests pass, but they have a maintenance problem. Every test repeats the same locators and login steps. If a developer changes a data-test attribute, you'd need to update every single test file that references it.
The Page Object Model (POM) solves this by centralizing page interactions into reusable classes. In this step, you'll create a LoginPage class and refactor your tests to use it.
In this step, get ready to:
- Create a LoginPage class that encapsulates all login page locators and actions.
- Refactor your login tests to use the page object instead of raw locators.
- Verify all tests still pass after the refactor.
Create the LoginPage class
A page object is a class that represents a single page in your application. It stores all the locators and wraps common interactions into methods. When the page changes, you update one class instead of dozens of tests.
- Create a pages/ directory inside your playwright-portfolio folder by running this command:
mkdir pages
- Confirm the folder exists by running:
ls
You should see pages listed alongside tests and node_modules in the output.
- Create a new file called LoginPage.ts inside the pages/ directory.
- In VS Code, right-click the pages folder in the Explorer sidebar and select New File.
- Name the file LoginPage.ts and press Enter.
- Add the following code to pages/LoginPage.ts:
import { type Locator, type Page } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
// Store each locator as a class property
this.usernameInput = page.locator('[data-test="username"]');
this.passwordInput = page.locator('[data-test="password"]');
this.loginButton = page.locator('[data-test="login-button"]');
this.errorMessage = page.locator('[data-test="error"]');
}
// Navigate to the login page
async goto() {
await this.page.goto('/');
}
// Fill credentials and click login
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
What does this code do?
This class is the single source of truth for how your tests interact with the login page.
- The constructor receives the Playwright page object and stores locators for every element the tests need (inputs, button, error message).
- goto() navigates to the login page. It uses the baseURL from your Playwright config, so '/' resolves to https://www.saucedemo.com/.
- login() fills both credential fields and clicks the login button in one call. Tests pass in different usernames and passwords to test different scenarios.
- Save pages/LoginPage.ts.
Why Page Object Model over raw locators?
Imagine the SauceDemo developers change [data-test="username"] to [data-test="user-input"]. Without POM, you'd update that selector in every test that logs in. With POM, you update one line in LoginPage.ts and every test that uses it is fixed automatically.
This is the pattern QA hiring managers look for because it shows you can write maintainable, scalable test code.
Seeing a red underline in VS Code?
- Make sure the file is saved as LoginPage.ts (not .js). TypeScript types require the .ts extension.
- Check that the file is inside the pages/ folder, not in the project root or inside tests/.
- Confirm @playwright/test appears in your package.json dependencies. If not, run npm install again.
Why is my LoginPage file showing TypeScript errors?
Refactor login tests to use the page object
Now that LoginPage exists, you'll rewrite your test file to use it. The tests will do the same thing, but the code will be shorter and more readable.
- Open tests/login.spec.ts in VS Code.
- Select all the existing code (press Cmd+A on macOS or Ctrl+A on Windows) and replace it with the following:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Login Page', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
// Create a fresh page object and navigate before each test
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login with standard user', async ({ page }) => {
await loginPage.login('standard_user', 'secret_sauce');
await expect(page).toHaveURL(/inventory/);
});
test('locked out user cannot login', async () => {
await loginPage.login('locked_out_user', 'secret_sauce');
await expect(loginPage.errorMessage).toContainText('locked out');
});
test('login fails with invalid credentials', async () => {
await loginPage.login('invalid_user', 'wrong_password');
await expect(loginPage.errorMessage).toBeVisible();
});
});
What changed in this refactor?
- The import at the top brings in your LoginPage class using a relative path (../pages/LoginPage).
- test.beforeEach creates a new LoginPage instance and navigates to the login URL before every test runs.
- Each test now calls loginPage.login() instead of repeating three locator calls. The assertions reference loginPage.errorMessage directly.
- Tests that only need the page object (not the raw page) no longer destructure { page } from the test arguments. This keeps the code clean.
- Save tests/login.spec.ts.
Seeing an import error?
- Double-check the import path is '../pages/LoginPage' (two dots to go up from tests/ into the project root, then down into pages/).
- Make sure the filename is exactly LoginPage.ts with a capital L and capital P. File imports are case-sensitive.
Help me fix my LoginPage import in the test file.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete pages/LoginPage.ts:
import { type Locator, type Page } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('[data-test="username"]');
this.passwordInput = page.locator('[data-test="password"]');
this.loginButton = page.locator('[data-test="login-button"]');
this.errorMessage = page.locator('[data-test="error"]');
}
async goto() {
await this.page.goto('/');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
And here is the complete refactored tests/login.spec.ts:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Login Page', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login with standard user', async ({ page }) => {
await loginPage.login('standard_user', 'secret_sauce');
await expect(page).toHaveURL(/inventory/);
});
test('locked out user cannot login', async () => {
await loginPage.login('locked_out_user', 'secret_sauce');
await expect(loginPage.errorMessage).toContainText('locked out');
});
test('login fails with invalid credentials', async () => {
await loginPage.login('invalid_user', 'wrong_password');
await expect(loginPage.errorMessage).toBeVisible();
});
});
Verify the refactor did not break anything
A successful refactor changes code structure without changing behaviour. All three tests should still pass with the same results as before.
- Run your test suite by running this command:
npx playwright test
You should see all 3 tests passing, just like before the refactor.
Tests failing after the refactor?
- Check that pages/LoginPage.ts exports the class with export class LoginPage. Without export, the import in your test file cannot find it.
- Make sure loginPage is declared with let at the top of the describe block (not inside beforeEach). It needs to be accessible from every test.
- Verify that both files are saved. Unsaved changes won't be picked up by the test runner.
My Playwright tests are failing after refactoring to Page Object Model.
- Commit your changes and push to GitHub by running these commands:
git add .
git commit -m "Refactor login tests with Page Object Model"
git push
Your tests are now cleaner and built on a professional pattern. Next up, you'll add a second page object and write tests for product interactions.
Test Product Interactions
Your login tests are solid and your LoginPage class keeps them clean and readable. Now it's time to test what happens after a user logs in.
The products page is where SauceDemo's e-commerce experience lives. In this step, you'll build a second page object and write tests that verify product display, cart interactions, and sorting.
In this step, get ready to:
- Create a ProductsPage class with locators and an add-to-cart method.
- Write 4 product tests covering display, cart, and sorting.
- Run all 7 tests and commit to GitHub.
Create the ProductsPage class
Just like LoginPage encapsulates the login form, you need a page object that encapsulates the products page. This keeps your test files focused on what to verify, not how to find elements.
- Create a new file called ProductsPage.ts inside your pages/ folder.
- Add the following code to pages/ProductsPage.ts:
import { type Locator, type Page } from '@playwright/test';
export class ProductsPage {
readonly page: Page;
readonly title: Locator;
readonly inventoryItems: Locator;
readonly cartBadge: Locator;
readonly sortDropdown: Locator;
constructor(page: Page) {
this.page = page;
// Locators target SauceDemo's data-test attributes
this.title = page.locator('[data-test="title"]');
this.inventoryItems = page.locator('[data-test="inventory-item"]');
this.cartBadge = page.locator('[data-test="shopping-cart-badge"]');
this.sortDropdown = page.locator('[data-test="product-sort-container"]');
}
// Finds a product by name and clicks its add-to-cart button
async addItemToCart(itemName: string) {
const item = this.page.locator('[data-test="inventory-item"]').filter({ hasText: itemName });
await item.locator('button').click();
}
}
What does this code do?
The class stores locators for the four elements you'll test: the page title, the list of inventory items, the cart badge counter, and the sort dropdown.
The addItemToCart method uses Playwright's .filter({ hasText: itemName }) to narrow down to a specific product card, then clicks its button. This approach is resilient because it matches by visible product name rather than a brittle index.
- Save pages/ProductsPage.ts.
File not saving?
Make sure the file is inside the pages/ directory (not tests/). Page objects live in pages/ and test specs live in tests/.
help me fix my ProductsPage file location
Write product test cases
Your tests need to log in before they can reach the products page. You'll reuse LoginPage inside a test.beforeEach hook so every test starts from an authenticated state.
- Create a new file called products.spec.ts inside your tests/ folder.
- Add the following code to tests/products.spec.ts:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { ProductsPage } from '../pages/ProductsPage';
test.describe('Products Page', () => {
let loginPage: LoginPage;
let productsPage: ProductsPage;
// Log in before every test so we start on the products page
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
productsPage = new ProductsPage(page);
await loginPage.goto();
await loginPage.login('standard_user', 'secret_sauce');
});
test('displays the products page after login', async () => {
await expect(productsPage.title).toHaveText('Products');
});
test('displays inventory items', async () => {
await expect(productsPage.inventoryItems.first()).toBeVisible();
});
test('can add an item to cart', async () => {
await productsPage.addItemToCart('Sauce Labs Backpack');
await expect(productsPage.cartBadge).toHaveText('1');
});
test('can sort products by price low to high', async () => {
// Select the 'lohi' option from the sort dropdown
await productsPage.sortDropdown.selectOption('lohi');
const firstItemPrice = productsPage.page.locator('[data-test="inventory-item-price"]').first();
await expect(firstItemPrice).toHaveText('$7.99');
});
});
What does this test file do?
The test.beforeEach hook logs in with standard_user before every test. This demonstrates a professional pattern: isolating test setup from test assertions.
- Test 1 verifies the page title reads "Products" after login.
- Test 2 confirms at least one inventory item is visible on the page.
- Test 3 adds the Sauce Labs Backpack to the cart and asserts the cart badge updates to "1".
- Test 4 selects "Price (low to high)" from the sort dropdown and checks the cheapest item shows $7.99.
- Save tests/products.spec.ts.
Seeing a TypeScript import error?
Check that the import path '../pages/ProductsPage' matches your file name exactly. TypeScript imports are case-sensitive, so ProductsPage must match the filename ProductsPage.ts.
help me fix my import error in products.spec.ts
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before running the tests.
ⓧ I'd like to double check the full code
Here is the full code for both new files:
import { type Locator, type Page } from '@playwright/test';
export class ProductsPage {
readonly page: Page;
readonly title: Locator;
readonly inventoryItems: Locator;
readonly cartBadge: Locator;
readonly sortDropdown: Locator;
constructor(page: Page) {
this.page = page;
this.title = page.locator('[data-test="title"]');
this.inventoryItems = page.locator('[data-test="inventory-item"]');
this.cartBadge = page.locator('[data-test="shopping-cart-badge"]');
this.sortDropdown = page.locator('[data-test="product-sort-container"]');
}
async addItemToCart(itemName: string) {
const item = this.page.locator('[data-test="inventory-item"]').filter({ hasText: itemName });
await item.locator('button').click();
}
}
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { ProductsPage } from '../pages/ProductsPage';
test.describe('Products Page', () => {
let loginPage: LoginPage;
let productsPage: ProductsPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
productsPage = new ProductsPage(page);
await loginPage.goto();
await loginPage.login('standard_user', 'secret_sauce');
});
test('displays the products page after login', async () => {
await expect(productsPage.title).toHaveText('Products');
});
test('displays inventory items', async () => {
await expect(productsPage.inventoryItems.first()).toBeVisible();
});
test('can add an item to cart', async () => {
await productsPage.addItemToCart('Sauce Labs Backpack');
await expect(productsPage.cartBadge).toHaveText('1');
});
test('can sort products by price low to high', async () => {
await productsPage.sortDropdown.selectOption('lohi');
const firstItemPrice = productsPage.page.locator('[data-test="inventory-item-price"]').first();
await expect(firstItemPrice).toHaveText('$7.99');
});
});
Run all tests and commit
You now have 7 tests across two spec files. Running them together confirms nothing is broken and both page objects work correctly.
- Run the full test suite by running this command:
npx playwright test
- You should see all 7 tests passing (3 from login.spec.ts and 4 from products.spec.ts).
What should I see?
Your terminal output shows "7 passed" with green checkmarks next to each test name. Playwright runs both spec files automatically because they're in the tests/ directory configured in playwright.config.ts.
- View the HTML report by running:
npx playwright show-report
- Your browser opens with a report showing all 7 tests grouped by spec file.
- Commit and push your changes by running these commands:
git add .
git commit -m "Add products page tests with POM"
git push
Tests failing?
If a test times out, SauceDemo might be temporarily slow. Try running again with npx playwright test --headed to watch the browser and spot where it stalls.
If you see a locator error, double-check the data-test values in your ProductsPage match the selectors used in the Complete Code above. SauceDemo uses data-test attributes on all interactive elements.
help me debug my failing Playwright product tests
Your test suite now covers two full pages of the application with clean page object separation. Next up, you'll add a GitHub Actions workflow and a professional README to make this repo shine for hiring managers.
Add GitHub Actions CI and Professional README
Your test suite is solid. You have 7 passing tests covering login and product interactions, all organized with the Page Object Model pattern.
Now it is time to make this portfolio shine for hiring managers. In this step, you will configure a GitHub Actions CI pipeline that runs your tests automatically on every push, and write a professional README that communicates your skills at a glance.
In this step, get ready to:
- Verify and configure the GitHub Actions workflow for automated test runs.
- Write a portfolio-ready README with a test coverage table and CI badge.
- Push to GitHub and confirm CI passes with a downloadable HTML report.
Configure the GitHub Actions workflow
GitHub Actions lets you define automated workflows that run on every push or pull request. Your playwright-portfolio already has a workflow file from the Playwright setup. You need to verify it matches the official Playwright CI configuration.
- In VS Code, open .github/workflows/playwright.yml from the Explorer sidebar.
- Replace the entire file contents with the following official Playwright CI workflow:
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
What does this workflow do?
- on: push/pull_request triggers the workflow every time you push code or open a pull request to main or master.
- npm ci installs your project dependencies in a clean CI environment.
- npx playwright install --with-deps downloads the Chromium browser binary plus system-level dependencies for running headless tests on Ubuntu.
- npx playwright test runs your full test suite.
- upload-artifact saves the HTML test report so you can download it after each run, even if tests fail.
- Save .github/workflows/playwright.yml.
Why CI matters for a QA portfolio
Hiring managers want to see that your tests run automatically, not just locally on your machine. A green CI badge on your repo proves your tests are reliable and always passing.
Write a professional README
A clear README is what separates a polished portfolio from a pile of code files. It tells hiring managers what you built, how to run it, and what skills you demonstrated.
- In VS Code, open README.md from the Explorer sidebar. If the file does not exist, create it in the root of your playwright-portfolio folder.
- Replace the entire file contents with the following portfolio-ready README:
# Playwright E2E Test Portfolio
Automated end-to-end test suite for [SauceDemo](https://www.saucedemo.com) built with Playwright and TypeScript.

## Tech Stack
- **Framework:** Playwright
- **Language:** TypeScript
- **CI/CD:** GitHub Actions
- **Pattern:** Page Object Model (POM)
- **Browsers:** Chromium (configurable for Firefox and WebKit)
## What This Tests
| Area | Tests | What It Verifies |
|------|-------|------------------|
| Login | 3 tests | Successful login, locked-out user error, invalid credentials error |
| Products | 4 tests | Page title, inventory display, add-to-cart, sort functionality |
## Project Structure
```
playwright-portfolio/
├── .github/workflows/ # GitHub Actions CI configuration
├── pages/ # Page Object Model classes
│ ├── LoginPage.ts
│ └── ProductsPage.ts
├── tests/ # Test specifications
│ ├── login.spec.ts
│ └── products.spec.ts
├── playwright.config.ts # Playwright configuration
└── README.md
```
## Running Locally
```bash
npm ci
npx playwright install
npx playwright test
npx playwright test --headed
npx playwright show-report
```
## CI/CD
Tests run automatically on every push and pull request via GitHub Actions. The HTML report is uploaded as an artifact on each run.
What makes this README effective?
- The CI badge at the top gives instant visual proof that tests pass.
- The test coverage table shows exactly what scenarios you automated without making the reader dig through code.
- The project structure diagram demonstrates you organize code professionally using Page Object Model.
- The "Running Locally" section lets anyone clone your repo and verify your tests in seconds.
- Save README.md.
Personalize later
You can add an "About Me" section and a "Skills Demonstrated" list to the README after completing the project. Focus on getting CI working first.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both files before moving on to the push.
ⓧ I'd like to double check the full code
Here is the full content of both files for comparison.
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
# Playwright E2E Test Portfolio
Automated end-to-end test suite for [SauceDemo](https://www.saucedemo.com) built with Playwright and TypeScript.

## Tech Stack
- **Framework:** Playwright
- **Language:** TypeScript
- **CI/CD:** GitHub Actions
- **Pattern:** Page Object Model (POM)
- **Browsers:** Chromium (configurable for Firefox and WebKit)
## What This Tests
| Area | Tests | What It Verifies |
|------|-------|------------------|
| Login | 3 tests | Successful login, locked-out user error, invalid credentials error |
| Products | 4 tests | Page title, inventory display, add-to-cart, sort functionality |
## Project Structure
```
playwright-portfolio/
├── .github/workflows/ # GitHub Actions CI configuration
├── pages/ # Page Object Model classes
│ ├── LoginPage.ts
│ └── ProductsPage.ts
├── tests/ # Test specifications
│ ├── login.spec.ts
│ └── products.spec.ts
├── playwright.config.ts # Playwright configuration
└── README.md
```
## Running Locally
```bash
npm ci
npx playwright install
npx playwright test
npx playwright test --headed
npx playwright show-report
```
## CI/CD
Tests run automatically on every push and pull request via GitHub Actions. The HTML report is uploaded as an artifact on each run.
Push and verify CI
With both files ready, push everything to GitHub and watch the workflow run your tests automatically in the cloud.
- Commit and push all changes by running these commands in your terminal:
git add .
git commit -m "Add CI workflow and professional README"
git push
What happens after the push?
Because your workflow triggers on push to main or master, GitHub Actions immediately picks up your commit and starts running the test job.
- Open your playwright-portfolio repository on GitHub in your browser.
- Click the Actions tab under the repository name.
- In the left sidebar, click Playwright Tests.
- Click the most recent workflow run to see the run summary.
- Wait for the run to complete. You should see a green checkmark indicating all tests passed.
- Scroll down to the Artifacts section on the run summary page.
- Click playwright-report to download the HTML report zip file.
What should I see?
The workflow run page shows each step (checkout, install, test, upload) with green checkmarks. The Artifacts section at the bottom lists playwright-report as a downloadable zip. Inside the zip is your full HTML test report with results for all 7 tests.
Workflow not running or failing?
- If you do not see the Actions tab, make sure your repository is public. GitHub Actions is free for public repos.
- If the workflow fails at "Install Playwright Browsers", check that your package.json and package-lock.json are both committed and pushed.
- If tests fail in CI but pass locally, check that baseURL in playwright.config.ts is set to https://www.saucedemo.com and not localhost.
Help me debug my GitHub Actions workflow
Your portfolio is complete. You have a professional test suite, clean architecture, automated CI, and a README that tells the story. Next up, take on the Secret Mission to extend your test coverage even further.
Secret mission
Add a Checkout Flow Test
Your test suite covers login and products, but real users don't stop there. They check out. Add a multi-step checkout flow test that chains three page objects together, proving you can automate complex user journeys end-to-end.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses GitHub Actions on a public repository, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- playwright-portfolio local project folder on your Desktop.
- playwright-portfolio GitHub repository.
- Playwright browser binaries (Chromium) cached on your machine.
Keep everything running
No action needed. Choose this if you plan to keep building on your test suite or want to reference it for job applications.
- Your local playwright-portfolio folder stays on your Desktop.
- Your GitHub repository remains public and your GitHub Actions workflow continues to run on each push.
- Playwright browsers remain cached locally (approximately 200-400 MB of disk space).
Pause - I'll come back to this later
Free up disk space while keeping your project files intact. You can reinstall browsers later with a single command.
- Remove the cached Playwright browsers by running this command inside your playwright-portfolio folder:
npx playwright uninstall
- Your project files and GitHub repo stay untouched. When you return, run npx playwright install to re-download the browsers.
Delete - I don't want to use this again
Remove all project resources from your machine and GitHub. This cannot be undone.
- Remove the cached Playwright browsers by running:
npx playwright uninstall
- Delete the local project folder by running:
cd ~/Desktop
rm -rf playwright-portfolio
Windows users
Use this command instead to delete the folder:
cd ~\Desktop
rmdir /s /q playwright-portfolio
- Delete the GitHub repository by opening your playwright-portfolio repo on GitHub.
- Click the Settings tab at the top of the repo page.
- Scroll to the bottom to the Danger Zone section.
- Click Delete this repository and follow the confirmation prompts.
Nice Work!
Nice Work!
Nice work! You just built a professional QA automation portfolio from scratch using Playwright and TypeScript. Your GitHub repository now demonstrates the exact skills hiring managers look for in test automation engineers.
You've learned how to:
- Build a complete Playwright E2E test suite with positive and negative test cases covering login, product interactions, and sorting.
- Implement the Page Object Model design pattern to structure test code the way professional test frameworks do.
- Set up a GitHub Actions CI pipeline that automatically runs tests on every push and publishes an HTML report artifact.
- Secret Mission: Added a multi-step checkout flow test demonstrating complex user journey automation with chained page objects.
Ready to quiz yourself?