Project Brief — Build a Type-Safe Page Object Framework

10 min read

Seven chapters of theory deserve one chapter of practice. The capstone project is a small but complete type-safe page object framework for a multi-page e-commerce application — the kind of framework you'd write on day one of a real QA engagement. It pulls in everything you've learned: interfaces, generics, utility types, enums vs literal unions, type guards, async function typing, and the page object / fixture / factory patterns from chapter 7. By the end you'll have a runnable codebase that's organised the way production test frameworks actually are organised, and a portfolio piece that demonstrates each concept this course covered.

This first lesson is the brief — what you're building, why each piece exists, and which TypeScript skill each piece exercises. The next lesson walks the implementation step by step. The third lesson is the self-review and stretch goals.

The application under test

A made-up e-commerce site with six pages — small enough to finish in a weekend, broad enough to exercise every framework component:

  • Login — email + password form, redirects to product list on success.
  • Product List — paginated grid of products with filter controls.
  • Product Detail — single product with quantity selector and "Add to cart" button.
  • Cart — list of cart items, quantity adjusters, totals, "Checkout" button.
  • Checkout — shipping form, payment form, "Place order" button.
  • Order Confirmation — order summary, confirmation number, "Continue shopping."

You don't need a real backend. The framework targets URLs and selectors; whether they exist in a real app, a stubbed app, or a Playwright trace replay is up to you. The exercise is the framework, not the app.

What you're building

Six concrete deliverables. Tick each one off as you go:

  1. Type definitions — interfaces for User, Product, CartItem, Order, plus a generic ApiResponse<T>. Utility-type variants (CreateUserInput, UpdateUserInput) derived from the base interfaces.
  2. Page objects — one class (or typed object) per page. Every locator typed, every method's return type explicit. A small base class for shared navigation.
  3. Test data factoriescreateUser, createProduct, createOrder using the generic factory pattern from chapter 7. A createMany<T> helper for bulk data.
  4. API helpers — typed wrappers for seeding data: login(creds): Promise<ApiResponse<AuthToken>>, plus a generic apiRequest<T>(endpoint, opts): Promise<ApiResponse<T>>.
  5. Configuration — an environment union ("local" | "staging" | "production") with a getBaseUrl helper, a browser config type, a timeout configuration interface.
  6. Sample tests — at least five test cases that use the framework end-to-end. Login, product browse, add to cart, checkout, order confirmation — one each, plus a sixth that demonstrates a type-safe failure scenario.

Suggested project structure

e2e-framework/
├── src/
│   ├── types/         (interfaces and type definitions)
│   ├── pages/         (page objects — LoginPage, ProductListPage, ...)
│   ├── factories/     (test data factories — createUser, createProduct, ...)
│   ├── api/           (typed API helpers)
│   ├── config/        (environment and browser config)
│   └── utils/         (shared utilities — type guards, assertions, formatters)
├── tests/             (sample tests using the framework)
├── tsconfig.json      (strict: true, the same as chapter 1)
└── package.json

This separation is deliberate. Each folder is a different kind of code, and each kind tends to evolve at a different rate. Page objects change when the app's UI changes; factories change when the data shape changes; API helpers change when the backend evolves; config changes per environment. Keeping them apart means a UI change touches one folder, not five.

Skills you're exercising — and where

Capstone framework
  • – User, Product, CartItem, Order
  • – ApiResponse<T>
  • – Omit, Pick, Partial variants
  • – createFactory<T>
  • – createMany<T>
  • – apiRequest<T>
  • – Class-based page objects
  • – Typed locators
  • – Promise<void> return types
  • isApiError(res) –
  • Discriminated union responses –
  • Custom assertion functions –
  • Environment union –
  • Browser literal type –
  • Order status union –

Every chapter from 2 onwards shows up. If you can finish the capstone, you can write the test framework for a real product.

Constraints — what makes the project type-safe, not just typed

A typed project has type annotations everywhere. A type-safe project uses those types to prevent real classes of mistakes. The capstone deliberately picks constraints that exercise the difference:

  • strict: true in tsconfig.json. No any, no implicit undefined, no quietly nullable values. The flag from chapter 1 earns its keep.
  • No raw selectors in test files. Every data-testid or CSS selector lives behind a page object boundary. A test that contains a string selector is a failed deliverable.
  • No raw fetch calls in test files. Every API call goes through apiRequest<T>. A test that wraps fetch directly is a failed deliverable.
  • Every public function has an explicit return type. Inferred returns are fine in the body of a function; the contract of an exported function should be visible at its signature.
  • At least one custom type guard. Use it inside a test or an assertion. Pick a place where you need to narrow a union — e.g. an ApiResponse<T> | ApiError discriminated union.

These aren't arbitrary — they're the rules that catch real bugs in real test suites.

Stretch goals

Three optional extensions, each escalating in difficulty:

  • Fluent builder API for test data. Instead of createUser({ role: "admin" }), support UserBuilder.create().withRole("admin").withEmail("x@y.com").build(). The fluent style makes intent obvious in tests; the typing challenge is making each method return the builder typed correctly.
  • Custom type guards for API responses. Add isAuthSuccess(res: ApiResponse<AuthToken> | ApiError): res is ApiResponse<AuthToken>. Use it in a test — show the narrowing payoff.
  • Conditional response types. A clever (and optional) trick: type ResponseData<T extends Endpoint> = T extends "user" ? User : T extends "product" ? Product : never. Wire it into apiRequest<T> so the response type is inferred from the endpoint string. This is the kind of advanced typing you saw briefly in chapter 6's mapped and conditional types lesson — the capstone is your chance to use it on a real call.

If you finish the core deliverables, attempt at least one stretch goal. They're where TypeScript stops being notation and starts being a tool that shapes how the framework can be used.

Time budget

A realistic range for someone working through the chapters in order:

  • Core deliverables (1-6): 6-10 hours, split over 2-3 sessions.
  • First stretch goal: 1-2 hours.
  • All three stretch goals + strict-mode audit: 4-6 hours.

Don't aim for perfection on the first pass. The point is to use every concept; you'll iterate on the framework once you see your tests using it.

Deliverable for self-assessment

By the end you should be able to answer "yes" to all of these:

  • The framework compiles with tsc --noEmit under strict: true with zero errors.
  • Each page object has typed methods with explicit return types — no any, no inferred Promise<unknown>.
  • Test data factories use generics and Partial<T> overrides — no copy-pasted defaults.
  • API helpers return Promise<ApiResponse<T>> — every test consumer gets the precise shape via the generic.
  • Tests read as a sequence of typed calls — no raw selectors, no raw fetches, no as any escape hatches.
  • At least one custom type guard or assertion function lives in src/utils/ and is used by a test.

The next lesson walks each step of the build with complete code you can copy and adapt. Read it once end-to-end before you start writing — the order of operations matters, and the components reference each other.

// tip to track lessons you complete and pick up where you left off across devices.