This episode covers testing a Remix application: unit testing React components with Vitest, integration testing for loaders and actions, E2E testing with Playwright, and accessibility testing with a thorough quality audit.

An application that works today can break tomorrow — because of code changes, upgraded dependencies, or scenarios you didn't think of while writing. Episode 16 is your safety net: testing and quality.
The good news: Remix's architecture makes testing easier than an SPA. Loaders and actions are pure functions that receive a request and return a response — very easy to test without a browser. React components are tested with Testing Library as usual. And for end-to-end flows, Playwright handles the rest.
Episode 16 builds the testing pyramid: unit, integration, E2E, then accessibility testing as a quality audit.
Vitest is a test runner that aligns with Vite — the most natural choice for Remix v3. Install and configure it in vite.config.ts:
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdomVitest configuration is added to vite.config.ts using a test block with the jsdom environment. After that, test files can be written immediately.
A unit test verifies one component with one behavior. Testing Library emphasizes testing from the user's point of view:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { TombolSuka } from "./TombolSuka";
describe("TombolSuka", () => {
it("menampilkan status awal", () => {
render(<TombolSuka sudahSuka={false} />);
expect(screen.getByRole("button")).toHaveTextContent("Suka");
});
it("mengganti teks saat diklik", async () => {
const user = userEvent.setup();
render(<TombolSuka sudahSuka={false} />);
await user.click(screen.getByRole("button"));
expect(screen.getByRole("button")).toHaveTextContent("Batal suka");
});
});screen.getByRole finds elements through their accessibility role, not any text. This makes the test enforce accessibility at the same time — a button that can't be found by role is usually also hard to reach for screen readers.
Test behaviors users can see: render, interaction, and state. Don't test implementation details like internal function names or fragile DOM structure. A good test doesn't break when the implementation is refactored.
A loader is an ordinary function that receives context and returns data. Test it directly by calling it and checking the result — no browser needed:
import { describe, expect, it } from "vitest";
import { loader } from "./app/routes/posts._index";
describe("loader posts", () => {
it("mengembalikan daftar posting", async () => {
const hasil = await loader({ request: new Request("http://lokal/") });
const data = await hasil.json();
expect(Array.isArray(data.posts)).toBe(true);
});
});A loader is tested with a fabricated Request, then the result is read as JSON. This approach is fast and requires no infrastructure.
For deterministic tests, mock the database service or use a separate test database. Prisma can be mocked or pointed at a test database via the DATABASE_URL env var. The key to determinism: every test must be repeatable without depending on another test's state.
E2E tests exercise complete flows like a real user: open a browser, click, fill forms, and check results. Playwright is a stable choice for this.
npm init playwright@latest
npx playwright testnpx playwright test runs all scenarios from the e2e folder. A webServer config in playwright.config can start the dev server automatically before the tests.
Pick critical flows: registration then login, creating a post, and checkout. Playwright waits for elements automatically, so tests mimic realistic user behavior. Start with the main flows; details can follow later.
Accessibility is part of quality, not a bonus feature. @axe-core/playwright can inject automatic checks into E2E tests:
npm install -D @axe-core/playwrightAxe automatically checks dozens of WCAG rules and reports violations. Run it on every main page.
Automated tools don't catch everything. Do regular manual audits: keyboard navigation without a mouse, color contrast, and logical heading structure. The combination of automated and manual checks keeps accessibility standards up over the long term.
Episode 16 builds the quality pyramid: component unit tests with Vitest, loader and action integration tests, E2E with Playwright, plus automated and manual accessibility testing. Your application can now grow without fear of breaking silently.
The key takeaways:
In the next episode, episode 17, we'll discuss error handling and UX — error boundaries and fallback UI, user-friendly error pages, pending and error states at the request level, and proper error reporting. Tests cover the known things; error handling covers the unknown ones.