Playwright Project: Build an E-commerce E2E Framework with TypeScript

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.

Advertisement

๐Ÿ“– 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

AreaScenarios
LoginValid login, locked-out user, wrong password, empty fields (data-driven)
InventoryProduct count, sorting A-Z, Z-A, price low-high, price high-low
CartAdd one, add several, remove, badge count, cart page contents
CheckoutRequired-field validation, price summary (item total, tax, total), order completion
SessionLogout, protected pages redirect to login when logged out

Technical requirements


๐Ÿ“ 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
  • [ ] @smoke suite 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

CriterionWeight
Coverage of the required scenarios25%
Framework design (POM, fixtures, data, structure)25%
Reliability (no flaky tests, no hard waits)20%
Readability (naming, test titles, tags)15%
CI and reporting15%

๐Ÿš€ Stretch Goals

  • Visual tests for the inventory and cart pages (visual testing).
  • Tests for problem_user that document the known UI bugs (use test.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

๐ŸŽฌ 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”

Leave a Comment