Testing Product Workflows with Vitest and Playwright

I do not want a test suite that only proves functions work. I want it to protect the workflows that would hurt if they broke.

That does not mean every rule needs a browser test. Browser tests are valuable, but they are slower and more fragile than service tests. If I can test a product rule directly with Vitest, I usually start there.

For example, a request may not be submitted without a title.

test("cannot submit an empty request", async () => {
  await expectSubmitRequest({ title: "" }).rejects.toThrow("Title is required");
});

This is a simplified test, but it shows the level where the rule belongs. I do not need to open a browser to check that an empty title is invalid. A service test can run quickly and fail close to the problem.

Playwright is useful for a different question: does the full path work for a user? Can they open the page, fill the form, submit it, follow the redirect, and see the new status?

That kind of test catches broken wiring. It catches missing form names, redirect mistakes, route loader errors, permission issues, and UI states that are not visible from a service test.

I try to keep browser tests focused on critical paths:

  • create the thing users create most often
  • edit the thing users are likely to edit
  • submit or approve a workflow
  • check the state that confirms the action worked
  • cover one important permission or error path

The mistake I have made before is pushing too much into end-to-end tests because they feel closer to the real product. That can turn the test suite into a slow checklist. When a test fails, it is not always clear whether the rule is wrong, the selector changed, the seed data broke, or the browser path is flaky.

A better split is:

  • Vitest for rules, services, validation, formatting, and small integration boundaries
  • Playwright for a small number of user journeys where the whole stack matters

Screenshots and traces can help when a browser test fails. They are especially useful in CI, where nobody is watching the browser. But private screenshots should stay private. For public writing, I would rather use a small demo app or no screenshot at all.

The trade-off is that test boundaries need maintenance. If the service layer is too thin, service tests do not prove much. If the UI owns too much logic, browser tests become the only way to test product behavior. That is usually a sign that the code boundary should improve, not that the test suite needs more browser coverage.

I also try to avoid fake confidence. A passing test suite does not prove the product is correct. It proves specific expectations are still true. The value is choosing expectations that match the risk users actually care about.

For a small product workflow, a useful test plan might be:

service test: cannot submit invalid input
service test: valid submit changes status
service test: user without permission is rejected
browser test: user creates, submits, and sees submitted status

That is not exhaustive. It is a practical start. It protects the rule and the path without making every small variation a full browser test.

Related Posts

Astro for Documentation and a Professional Site

I use Astro because this site is mostly writing. I do not need a heavy app framework for pages that should load fast and be easy to edit. That sounds simple, but it is the mai

read more

Localization in Product Apps

Localization is not only replacing English strings with another language. In a product app, language touches workflow. It changes labels, validation messages, dates, empty states, permissions copy, d

read more

MCP as a Safe AI Integration Boundary

MCP is interesting because it makes AI integrations feel less like prompt magic and more like software boundaries. That is the part I care about. A model should no

read more

Zod, OpenAPI, and Swagger for API Contracts

A public API is not just backend code. It is a product surface for another developer. That means the contract has to be readable. It also has to be enforced at runtime. Types in the app are useful, b

read more

pg-boss for Durable Background Jobs

The customer problem was not "we need a queue". The problem was that a slow operation made the user wait with no clear answer. That distinction matters. A queue is an implementation detail. The produ

read more

Pragmatic Drag and Drop for Real Ordering Tasks

Drag and drop is easy to add for a demo and harder to make reliable for real work. The product question is not "can the item move on screen?" The question is whether the user can safely change an ord

read more

Prisma and PostgreSQL as the Product Source of Truth

I do not think of PostgreSQL as only infrastructure. In a product app, it is where the product remembers what happened. That makes database design a product decision. I

read more

React Router for Full-Stack Product Workflows

A route is not only a URL. In a product app, a route often represents a task the user is trying to finish. That sounds obvious, but it changes how I design the code. A settings page that starts an im

read more

shadcn-Style UI as an Owned Product System

I like copied UI primitives because they make the component library feel like part of the app, not something the app is borrowing. That is the part of the shadcn/ui-style ap

read more

Dense Operational UI with Tables and Editors

Sometimes a simple form is the wrong UI. If the user needs to compare many values and make careful edits, a table can be kinder than a long page of inputs. Dense UI has a bad reputation when it is us

read more

Vercel AI SDK with Explicit Tool Boundaries

The risky part of an AI feature is not the chat UI. The risky part is what the chat is allowed to do. It is easy to make an assistant feel powerful by giving it tools. With something like the [Vercel

read more

Vertical Slice Architecture with Dependency-Cruiser

I like vertical slices because they make a feature easier to delete, move, or review. The folder structure is not the main value. The value is that the code for one workflow is not spread across ten u

read more

Zod Beyond Validation

Zod is usually introduced as a validation library. That is true, but the more useful idea is boundary definition. A TypeScript type only helps after data is already inside the pro

read more