Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

Testing React components against the worst case

I wrote this with AI help and edited it myself. Every example is a real fixture from the Brio UI repo. The demo for a component is always a good day. Three plan names that fit, prices in dollars, an FAQ with six short q

Testing React components against the worst case

I wrote this with AI help and edited it myself. Every example is a real fixture from the Brio UI repo.

The demo for a component is always a good day. Three plan names that fit, prices in dollars, an FAQ with six short questions in English. You build to that demo, it looks right, and you ship.

Then someone pastes a German product name into the pricing card, or the FAQ grows to 120 entries, or the page gets translated into Arabic. That is the first time the component sees real data, and it's usually on a phone.

I build Brio UI, a library of 50 React components, and I stopped trusting demo data a while ago. Every component now has a second fixture whose only job is to break it. This post is the method. You can copy it into any component library or app.

Two fixtures per component

The idea comes from Emil Kowalski: try to break the component with the worst plausible real-world data, screenshot it, then fix what breaks.

In practice each component gets a file next to it, <slug>.stress.tsx, with three exports:

import type { ComponentType } from 'react'

export type StressModule = {
  Best: ComponentType   // the demo's data, no controls
  Worst: ComponentType  // same props shape, the nastiest plausible data
  cases: string[]       // one line per thing Worst throws at it
}

Best and Worst use the same public props. That rule matters. If the worst case needs a special prop or a patched component to render, you are testing a different component.

cases is a plain list of strings, shown on the test page above the fixture. It doubles as documentation: anyone can read what the component was tested against without opening the code.

What goes into the worst case

I keep a checklist of axes and go through every one that applies:

  • Long and unbroken text. Long titles and labels, plus strings with no spaces: URLs, email addresses, hashes, German compounds.
  • Big numbers and many items. 9,999,999 and 1e9, 120 rows instead of 6, and also the one-item and empty cases.
  • Missing optional fields. No avatar, no description, no date. Very short values like "Jo".
  • Scripts and direction. Japanese with no spaces, Arabic in a right-to-left page, Hindi, emoji sequences, combining marks.
  • Dates and time zones. Leap days, year 9999, an invalid date string, a negative offset.
  • States. Loading, error, disabled, expired, over the limit.

Here is part of the FAQ's list, as it appears in the file:

export const cases = [
  '120 questions, type multiple, 4 open on mount (+1 unknown id)',
  '200-char question with a 79-char German compound',
  'unbroken 130-char URL as a question, and inside answers',
  'emoji-only question (slug falls back to faq-question-N)',
  'Zalgo combining marks, very short "Jo?"',
  'javascript: contactHref (should be hidden)',
  'items = [] with search on (renders nothing)',
]

The changelog's worst case has 68 releases, a 65-character semver with build metadata, a leap day that doesn't exist (2026-02-29) and the string "not a date" as a date. The contact form gets a 78-character email address and an onSubmit that throws an empty error.

Two rules keep the fixtures useful. They must be deterministic, so no Date.now() or Math.random() during render; fixed date literals only. And if the component crashes on some input, the input stays in the fixture with a CRASH: note in cases. An error boundary shows the crash on the page. Hiding it defeats the point.

Looking at it

All fixtures load on one test page, #/stress/<slug>, with three modes (demo data, worst case, side by side) and three widths (full, 480 px, 360 px). The frame around the fixture never scrolls or clips. If something is too wide, it spills past a dashed edge where you can see it.

Then I take screenshots of the worst case and write down what is wrong: overflow, overlap, clipping, text wrapping into one-word lines, misalignment, unreadable contrast. Each gets a severity and a likely fix. Only then do I change the component.

Pricing Toggle worst case at 320 px: very long plan names wrapping instead of truncating

Some of what this caught on the way to release:

  • The contact form's worst case had 713 tap targets under the size I aim for. After the fix it had none.
  • The pricing toggle truncated long plan names. It wraps them now.
  • A comparison table clamped feature labels to three lines on narrow screens. That hid the end of long labels and clipped an Arabic word by 8 px. Labels now wrap in full.
  • The FAQ needed a 70-character measure, code that wraps on narrow screens and a list that doesn't trap the scroll.

The demo data for each of these looked fine the whole time.

Keeping it fixed

A fix you made once tends to break again three weeks later, when you change a padding value for an unrelated reason. So once a worst case looks right, I store screenshots of it as baselines: the Worst fixture at 320 px wide, in light and dark.

A script takes the same shots again on every run and compares them pixel by pixel. A pixel counts as changed when any colour channel moves by more than 32 of 255, which ignores anti-aliasing. A shot fails when more than 0.1% of its pixels change, or when its height changes by more than 4 px, because a height change means something re-wrapped. The same pass checks that the page's scrollWidth stays at 320 px, so horizontal overflow fails even if nothing else moved.

For scale: the pricing toggle's worst case is 320 by 2,820 px, so 0.1% is about one short word. Making a single plan name one line longer changes 12.5% of the image and fails.

Baselines depend on the browser build and the installed fonts. After a Chromium upgrade I refresh them all and look at every image before committing. When a visual change is intended, the new baseline goes into the same commit, and the image diff in the pull request becomes the review.

A slow CPU is a worst case too

Data isn't the only thing that can be worst-case. The same fixtures also run against a production build with the CPU throttled 10 times. A component that has finished animating should cost nothing at idle: no requestAnimationFrame loops, and infinite animations stay in CSS. Entrance staggers are capped, and long lists render in pages.

Try it on one component

You don't need a library to start. Pick the component in your app that shows user data, write a Worst next to it with the same props, and open it at 320 px. Go through the checklist above. It takes about an hour, and the first run usually finds something.

If you'd rather start from components that have already been through this, Brio UI's 10 free components are MIT and install with npx brio-ui add <name>. Pro launches in November, and joining the waitlist before 30 Nov gets you 50% off every Pro purchase.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.