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:
- Type definitions — interfaces for
User,Product,CartItem,Order, plus a genericApiResponse<T>. Utility-type variants (CreateUserInput,UpdateUserInput) derived from the base interfaces. - 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.
- Test data factories —
createUser,createProduct,createOrderusing the generic factory pattern from chapter 7. AcreateMany<T>helper for bulk data. - API helpers — typed wrappers for seeding data:
login(creds): Promise<ApiResponse<AuthToken>>, plus a genericapiRequest<T>(endpoint, opts): Promise<ApiResponse<T>>. - Configuration — an environment union (
"local" | "staging" | "production") with agetBaseUrlhelper, a browser config type, a timeout configuration interface. - 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
- – 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: trueintsconfig.json. Noany, no implicitundefined, no quietly nullable values. The flag from chapter 1 earns its keep.- No raw selectors in test files. Every
data-testidor 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 wrapsfetchdirectly 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> | ApiErrordiscriminated 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" }), supportUserBuilder.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 intoapiRequest<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 --noEmitunderstrict: truewith zero errors. - Each page object has typed methods with explicit return types — no
any, no inferredPromise<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 anyescape 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.