Time to build something real. In this project you will create a complete Playwright + TypeScript framework for an online store, from requirements and folder structure to page objects, fixtures, data-driven tests, cross-browser runs and a CI pipeline.
๐ What You’ll Learn: how to turn requirements into a test scope, project configuration with setup and browser projects, logging in once with storageState, page objects for inventory, cart and checkout, fixtures and typed test data, smoke and regression tests, CI, an acceptance checklist and an evaluation rubric.
๐ฏ Project Brief
You are the automation engineer for Swag Labs (https://www.saucedemo.com), an online store. Build a production-style Playwright + TypeScript framework that covers the store’s critical journeys and runs automatically in CI.
This project brings together everything from Modules 1 to 12. Work through it step by step; each step links back to the lesson that teaches the skill.
Time needed: 8 to 12 hours, spread over a week.
๐ Requirements
Functional coverage
| Area | Scenarios |
|---|---|
| Login | Valid login, locked-out user, wrong password, empty fields (data-driven) |
| Inventory | Product count, sorting A-Z, Z-A, price low-high, price high-low |
| Cart | Add one, add several, remove, badge count, cart page contents |
| Checkout | Required-field validation, price summary (item total, tax, total), order completion |
| Session | Logout, protected pages redirect to login when logged out |
Technical requirements
- Page Object Model with a
BasePage(POM) - Custom fixtures that inject page objects (custom fixtures)
- Log in once with a setup project and
storageState(login sessions) - Typed test data in separate files (parameterized tests)
- Tags
@smokeand@regression(tags) - Chromium and Firefox projects plus one mobile project (cross-browser)
- HTML report with traces on retry (reporters)
- GitHub Actions pipeline (GitHub Actions)
๐ Step 1: Create the Project and Folders
mkdir swaglabs-playwright && cd swaglabs-playwright
npm init playwright@latest
swaglabs-playwright/
โโโ .github/workflows/playwright.yml
โโโ src/
โ โโโ pages/
โ โ โโโ BasePage.ts
โ โ โโโ LoginPage.ts
โ โ โโโ InventoryPage.ts
โ โ โโโ CartPage.ts
โ โ โโโ CheckoutPage.ts
โ โโโ fixtures/index.ts
โ โโโ data/
โ โโโ users.ts
โ โโโ checkout.ts
โโโ tests/
โ โโโ auth.setup.ts
โ โโโ login.spec.ts
โ โโโ inventory.spec.ts
โ โโโ cart.spec.ts
โ โโโ checkout.spec.ts
โโโ playwright/.auth/ # add to .gitignore
โโโ playwright.config.ts
The layout follows framework folder structure and best practices.
โ๏ธ Step 2: Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const authFile = 'playwright/.auth/standard.json';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: process.env.CI ? [['dot'], ['github'], ['html', { open: 'never' }]] : [['list'], ['html', { open: 'on-failure' }]],
use: {
baseURL: 'https://www.saucedemo.com',
testIdAttribute: 'data-test',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: authFile }, dependencies: ['setup'] },
{ name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: authFile }, dependencies: ['setup'] },
{ name: 'mobile', use: { ...devices['Pixel 5'], storageState: authFile }, dependencies: ['setup'], grep: /@smoke/ },
],
});
๐ Note: Login tests must start logged out. In login.spec.ts add test.use({ storageState: { cookies: [], origins: [] } }) to ignore the saved session.
๐ Step 3: Log In Once (Setup Project)
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/standard.json';
setup('authenticate as standard user', async ({ page }) => {
await page.goto('/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill(process.env.SAUCE_PASSWORD ?? 'secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL(/inventory/);
await page.context().storageState({ path: authFile });
});
๐งฑ Step 4: Page Objects
// src/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
readonly cartBadge: Locator;
readonly cartLink: Locator;
constructor(protected readonly page: Page) {
this.cartBadge = page.locator('.shopping_cart_badge');
this.cartLink = page.locator('.shopping_cart_link');
}
protected abstract readonly path: string;
async open(): Promise<void> {
await this.page.goto(this.path);
}
}
// src/pages/InventoryPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export type SortOption = 'az' | 'za' | 'lohi' | 'hilo';
export class InventoryPage extends BasePage {
protected readonly path = '/inventory.html';
readonly title: Locator;
readonly items: Locator;
readonly sortSelect: Locator;
constructor(page: Page) {
super(page);
this.title = page.locator('.title');
this.items = page.locator('.inventory_item');
this.sortSelect = page.getByTestId('product-sort-container');
}
item(name: string): Locator {
return this.items.filter({ hasText: name });
}
async addToCart(...names: string[]): Promise<void> {
for (const name of names) {
await this.item(name).getByRole('button', { name: 'Add to cart' }).click();
}
}
async sortBy(option: SortOption): Promise<void> {
await this.sortSelect.selectOption(option);
}
async names(): Promise<string[]> {
return this.page.locator('.inventory_item_name').allTextContents();
}
async prices(): Promise<number[]> {
const texts = await this.page.locator('.inventory_item_price').allTextContents();
return texts.map(t => Number(t.replace('$', '')));
}
}
// src/pages/CartPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export class CartPage extends BasePage {
protected readonly path = '/cart.html';
readonly items: Locator;
readonly checkoutButton: Locator;
constructor(page: Page) {
super(page);
this.items = page.locator('.cart_item');
this.checkoutButton = page.getByTestId('checkout');
}
async remove(name: string): Promise<void> {
await this.items.filter({ hasText: name }).getByRole('button', { name: 'Remove' }).click();
}
}
// src/pages/CheckoutPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export interface CustomerInfo { firstName: string; lastName: string; postalCode: string }
export class CheckoutPage extends BasePage {
protected readonly path = '/checkout-step-one.html';
readonly error: Locator;
readonly itemTotal: Locator;
readonly tax: Locator;
readonly total: Locator;
readonly completeHeader: Locator;
constructor(page: Page) {
super(page);
this.error = page.getByTestId('error');
this.itemTotal = page.locator('.summary_subtotal_label');
this.tax = page.locator('.summary_tax_label');
this.total = page.locator('.summary_total_label');
this.completeHeader = page.locator('.complete-header');
}
async fillInformation(info: CustomerInfo): Promise<void> {
await this.page.getByTestId('firstName').fill(info.firstName);
await this.page.getByTestId('lastName').fill(info.lastName);
await this.page.getByTestId('postalCode').fill(info.postalCode);
await this.page.getByTestId('continue').click();
}
async finish(): Promise<void> {
await this.page.getByTestId('finish').click();
}
}
Write LoginPage yourself, following the example in the framework structure lesson.
๐งฉ Step 5: Fixtures and Test Data
// src/fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';
import { CartPage } from '../pages/CartPage';
import { CheckoutPage } from '../pages/CheckoutPage';
type Pages = { loginPage: LoginPage; inventoryPage: InventoryPage; cartPage: CartPage; checkoutPage: CheckoutPage };
export const test = base.extend<Pages>({
loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
inventoryPage: async ({ page }, use) => { await use(new InventoryPage(page)); },
cartPage: async ({ page }, use) => { await use(new CartPage(page)); },
checkoutPage: async ({ page }, use) => { await use(new CheckoutPage(page)); },
});
export { expect } from '@playwright/test';
// src/data/checkout.ts
import { CustomerInfo } from '../pages/CheckoutPage';
export const validCustomer: CustomerInfo = { firstName: 'Asha', lastName: 'Verma', postalCode: '411001' };
export const missingFieldCases: { name: string; info: CustomerInfo; error: string }[] = [
{ name: 'first name', info: { firstName: '', lastName: 'Verma', postalCode: '411001' }, error: 'First Name is required' },
{ name: 'last name', info: { firstName: 'Asha', lastName: '', postalCode: '411001' }, error: 'Last Name is required' },
{ name: 'postal code', info: { firstName: 'Asha', lastName: 'Verma', postalCode: '' }, error: 'Postal Code is required' },
];
๐งช Step 6: Tests
// tests/inventory.spec.ts
import { test, expect } from '../src/fixtures';
test.beforeEach(async ({ inventoryPage }) => {
await inventoryPage.open();
});
test('shows six products', { tag: '@smoke' }, async ({ inventoryPage }) => {
await expect(inventoryPage.items).toHaveCount(6);
});
test('sorts by price low to high', { tag: '@regression' }, async ({ inventoryPage }) => {
await inventoryPage.sortBy('lohi');
const prices = await inventoryPage.prices();
expect(prices).toEqual([...prices].sort((a, b) => a - b));
});
test('sorts by name Z to A', { tag: '@regression' }, async ({ inventoryPage }) => {
await inventoryPage.sortBy('za');
const names = await inventoryPage.names();
expect(names).toEqual([...names].sort().reverse());
});
// tests/checkout.spec.ts
import { test, expect } from '../src/fixtures';
import { validCustomer, missingFieldCases } from '../src/data/checkout';
test.beforeEach(async ({ inventoryPage, cartPage }) => {
await inventoryPage.open();
await inventoryPage.addToCart('Sauce Labs Backpack');
await inventoryPage.cartLink.click();
await cartPage.checkoutButton.click();
});
test('complete an order with correct totals', { tag: ['@smoke', '@regression'] }, async ({ checkoutPage }) => {
await checkoutPage.fillInformation(validCustomer);
await expect(checkoutPage.itemTotal).toHaveText('Item total: $29.99');
await expect(checkoutPage.tax).toHaveText('Tax: $2.40');
await expect(checkoutPage.total).toHaveText('Total: $32.39');
await checkoutPage.finish();
await expect(checkoutPage.completeHeader).toHaveText('Thank you for your order!');
});
for (const c of missingFieldCases) {
test('checkout requires ' + c.name, { tag: '@regression' }, async ({ checkoutPage }) => {
await checkoutPage.fillInformation(c.info);
await expect(checkoutPage.error).toContainText(c.error);
});
}
Write login.spec.ts (data-driven, logged out) and cart.spec.ts (add several, remove, badge) yourself.
๐ก Tip: Each test adds its own products after the saved login, so tests never depend on each other and can run fully in parallel (parallel execution).
๐ Step 7: Continuous Integration
Use the workflow from Run Playwright tests in GitHub Actions. Add playwright/.auth/ to .gitignore, push to GitHub and confirm that the pipeline runs, uploads the HTML report, and fails if you break a test.
โ Acceptance Checklist
- [ ] At least 20 tests across login, inventory, cart, checkout and session
- [ ] No selectors or URLs inside test files
- [ ] Login runs once per run (setup project)
- [ ] Data-driven login and checkout validation tests
- [ ]
@smokesuite runs in under 2 minutes - [ ] Passes in Chromium, Firefox and the mobile project
- [ ] Stable with
--repeat-each=5 - [ ] CI pipeline green, HTML report and traces available
๐ Evaluation Rubric
| Criterion | Weight |
|---|---|
| Coverage of the required scenarios | 25% |
| Framework design (POM, fixtures, data, structure) | 25% |
| Reliability (no flaky tests, no hard waits) | 20% |
| Readability (naming, test titles, tags) | 15% |
| CI and reporting | 15% |
๐ Stretch Goals
- Visual tests for the inventory and cart pages (visual testing).
- Tests for
problem_userthat document the known UI bugs (usetest.fail()). - Shard the pipeline into two jobs with a merged report.
- Ask the Playwright Test Agents to propose missing scenarios and compare with your plan.
โ FAQ
๐ค What is a good Playwright project for a portfolio?
An e-commerce framework like this one: page objects, fixtures, data-driven tests, cross-browser and mobile projects, a setup project for login, and a green CI pipeline on GitHub.
๐ค Should I put this project on GitHub?
Yes. A public repository with a clear README, a passing Actions badge and a short description of the design decisions is strong evidence in interviews.
๐ค Why use storageState instead of logging in before each test?
Logging in once and reusing the session makes the suite much faster and removes repeated UI steps that add flakiness.
๐ค How many tests should the project have?
Quality matters more than quantity. Around 20 to 30 well-designed, independent tests covering the scenarios above is a solid result.
๐ฏ Next Steps
- ๐๏ธ Project 3: API + UI Hybrid Framework with CI
- ๐ Module-wise Practice Assignments
- ๐ผ 200 Playwright + TypeScript Interview Questions
- ๐ญ Playwright Tutorial with TypeScript
๐ฌ Watch the full Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in
3 thoughts on “Playwright Project: Build an E-commerce E2E Framework with TypeScript”