تخطَّ إلى المحتوى

كل المنشورات / الاختبارات / №10

الاختبارات ناجحة والواجهة معطّلة: فهم JSDOM وVitest وPlaywright

قد يتحقق الاختبار من أمر ويغفل عمّا يهمّ المستخدم. ما الذي تستطيع JSDOM إثباته وما الذي لا تستطيعه، وكيف تُدخل Karma وVitest Browser Mode وPlaywright المتصفح الحقيقي في الاختبار، وطريقة لتوزيع الاختبارات تختار لكل اختبار البيئة التي تناسب ما يتحقق منه.

اقرأه على ميديوم

هذا المقال مكتوب بالإنجليزية، وملخصه بالعربية في الأعلى.

Why passing frontend tests don’t always guarantee a working user experience, and how to choose the right testing environment.

The problem with trusting green tests

We’ve all seen it: the test suite passes, the CI pipeline is green, and everything looks good. Then someone opens the application in a browser and discovers a bug that the tests never caught.

How does that happen?

The answer is that a test can correctly verify one thing while failing to verify something else that matters to the user.

In frontend development, this often comes down to the environment in which our tests run.

To understand why, let’s look at the evolution of some popular testing tools and the different problems they solve.

1. Jest and JSDOM: fast tests without a real browser

Jest became a popular choice for JavaScript testing because it provides a test runner, assertions, mocking utilities, and a convenient developer experience.

For frontend applications, Jest is often used with JSDOM.

JSDOM implements a substantial part of web standards related to the DOM and HTML in JavaScript. This allows tests to create elements, inspect attributes, dispatch events, and verify many component behaviors without launching an actual browser.

For example, we can test whether a button invokes a callback:

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, expect, test } from "vitest";

test("calls the handler when clicked", async () => {
  const user = userEvent.setup();
  const onClick = vi.fn();

  render(<button onClick={onClick}>Save</button>);

  await user.click(screen.getByRole("button", { name: "Save" }));

  expect(onClick).toHaveBeenCalledOnce();
});

This is a useful test. It verifies that the button’s interaction triggers the expected callback.

However, it doesn’t prove that the button is positioned correctly, visible in the viewport, or accessible to the user under every responsive layout.

Why? Because JSDOM doesn’t perform browser layout and rendering like a real browser.

That distinction becomes important when our application depends on CSS, viewport dimensions, scrolling, element positioning, or other browser-specific behavior.

2. When the DOM says yes, but the browser says no

Imagine a card component with a button inside it.

On desktop, the layout looks correct. On mobile, a responsive CSS rule causes the button to extend beyond the visible area of its container, which uses overflow: hidden.

The button still exists in the DOM.

A test that queries the DOM and directly invokes the click handler might pass. But the actual user may not be able to see or interact with the button as intended.

There are two separate questions here:

  • Does the button exist, and does its handler work?
  • Can a user actually reach and interact with the button in the rendered interface?

The first question can often be answered using a simulated DOM environment.

The second may require a real browser.

One important nuance: not every test that passes under JSDOM would pass in a browser, and not every DOM-based test has this limitation. The problem appears when a test relies on behavior that the simulated environment doesn’t reproduce.

The lesson is not that JSDOM is bad. It’s that we need to understand what our testing environment can, and cannot, prove.

3. Karma and the move toward real browsers

Karma became a popular test runner, particularly in Angular ecosystems, by making it possible to execute tests in actual browsers.

This introduced an important advantage: tests could run against real browser implementations rather than relying entirely on a simulated DOM.

When browser layout, native APIs, event behavior, or browser-specific differences matter, running the test in a real browser can reveal problems that a simulated environment misses.

Of course, using a real browser doesn’t automatically make every test better. Browser-based testing can require more setup and resources, and it still depends on what the test actually asserts.

Karma and Jest also aren’t exact substitutes for one another: they serve different roles in the testing ecosystem.

The broader shift was toward recognizing that different testing environments provide different levels of confidence.

4. Vitest: one testing framework, multiple environments

Vitest brought a modern testing experience closely integrated with Vite, with fast execution, familiar testing APIs, and support for multiple testing environments.

It can run tests in environments such as Node.js and JSDOM, and it also provides Browser Mode.

Browser Mode allows tests to run inside an actual browser through a configured provider, including Playwright and WebdriverIO.

This means we don’t necessarily have to choose between a convenient testing workflow and the ability to verify browser-dependent behavior.

We can use different test projects or configurations for different purposes.

For example:

  • Run isolated logic tests in Node.js.
  • Run component tests in JSDOM when browser layout isn’t relevant.
  • Run browser tests when actual rendering or native browser behavior matters.

Vitest doesn’t eliminate the need for other testing tools. Instead, it gives us another flexible way to organize our testing strategy.

For implementation details, see the official Vitest Browser Mode documentation.

5. Why Playwright still matters

Playwright is widely used for End-to-End testing because it automates real browsers and allows tests to interact with applications in ways that resemble actual user behavior.

A typical E2E test might:

  • Open the application.
  • Navigate to a page.
  • Fill in a form.
  • Submit it.
  • Verify the resulting UI or navigation.

Playwright can also support component testing, where components run in a real browser and tests can verify actual layout and interactions.

This makes it useful when we need to validate behavior beyond what a simulated DOM can provide.

However, Browser Mode in Vitest and Playwright Test are not identical products. Vitest Browser Mode lets you run tests using Vitest’s testing APIs inside a browser. Playwright Test provides its own runner, browser automation APIs, fixtures, assertions, and E2E workflow.

The right choice depends on whether we’re testing an isolated piece of behavior, a component in a real browser, or a complete user journey.

For more details, see the official Playwright documentation.

6. Unit tests vs. integration tests vs. E2E tests

These terms describe different testing scopes, not necessarily different tools.

Unit tests focus on small, isolated pieces of functionality.

Examples include:

  • Calculating a price or discount.
  • Validating a data transformation.
  • Testing a utility function.
  • Verifying a component’s isolated logic.

These tests are usually fast and should make it easy to pinpoint the source of a failure.

Integration tests verify that multiple parts of a system work together.

For example:

  • A form component interacts correctly with validation.
  • A component updates when application state changes.
  • Several modules cooperate to complete a workflow.

Integration tests can run in JSDOM or a real browser, depending on the behavior being tested.

Browser and E2E tests: browser tests verify behavior in an actual browser. E2E tests generally cover larger workflows across the application.

Examples include:

  • A responsive menu opens and closes correctly.
  • A button is actually reachable and clickable.
  • A form submission produces the expected result.
  • A user can complete a checkout flow.

These categories overlap in practice. A browser test can verify an isolated component, while an E2E test usually exercises a broader slice of the application.

The important question is not simply which tool we’re using, but what level of behavior we’re verifying.

7. A practical testing strategy

I don’t think every test should run in a real browser. Doing so can increase execution time, infrastructure requirements, and maintenance costs.

Instead, I prefer thinking about testing as a set of complementary layers.

  • Pure business logic: Vitest or Jest in Node.js
  • DOM and component behavior: Vitest or Jest with JSDOM
  • Browser-dependent component behavior: Vitest Browser Mode or another browser testing setup
  • Complete user journeys: Playwright Test

For a responsive UI bug, I would add a browser-level test that checks whether the element is actually visible and interactable at the relevant viewport size.

For business logic, I would keep the test fast and isolated.

For a critical user flow, I would use an E2E test to verify that the different parts of the application work together.

This approach gives us useful coverage without making every test unnecessarily expensive.

Final thoughts

Frontend testing isn’t just about getting a green CI pipeline. It’s about building confidence that our application behaves correctly under the conditions that matter.

JSDOM is valuable because it makes many tests fast and convenient. Real browsers are valuable because they expose behavior that a simulated DOM cannot fully reproduce. Vitest and Playwright offer different ways to cover these needs.

The goal isn’t to replace one tool with another.

The goal is to match the testing environment to the behavior we need to verify.

Because a passing test is only meaningful when we understand exactly what it has proven.

Further reading

Esc

↑↓ للتنقل · Enter للفتح · Esc للإغلاق