DEV Community

Cover image for Our Tests Were Green. The Feature Had Never Worked.
Diven Rastdus
Diven Rastdus

Posted on • Originally published at astraedus.dev

Our Tests Were Green. The Feature Had Never Worked.

I was doing a UI cleanup pass on our astrology companion app. One card said "Recurring themes are coming soon" in a PRO section. I restyled it to match the new design and moved on.

Our tech lead caught it an hour later.

The feature wasn't "coming soon." It was built. Three months ago. The component existed, the data pipeline existed, the edge function computed the themes and returned them. But a parent component never passed the themes prop. One unwired seam. The card had been silently dead for every paying user since launch.

The worst part: tsc was clean. Unit tests were green. There was no error to find.

Here's what the broken wiring looked like:

// What shipped -- tsc passes, component tests pass, no warnings
<MirrorCard isPro />          // themes prop optional, defaults to []

// What a paying user needed
<MirrorCard isPro themes={insights.recurringThemes} />
Enter fullscreen mode Exit fullscreen mode

One token. A green test suite and a one-token-wide hole.


The problem is never the piece, it's the seam

Every unit test verifies a piece in isolation. Pass in these inputs, expect these outputs. That's the right thing to test.

But user-facing features aren't pieces. They're chains: edge function computes data, API response carries it, parent receives it, parent passes it to the component, component renders it, button routes somewhere real. A test can verify every link in that chain individually while the chain itself is broken.

The gap is the seam. And seams are invisible to your test suite.

After we found the themes card, we ran a completeness audit and found the same pattern in three more places:

Live transit data that never rendered. The edge function computed the day's planetary transits correctly. Tests passed. But the response body didn't include the transits field, so the Today screen's chips never had data to render. Empty chips, no error, zero indication anything was wrong.

A "PRO unlock" that didn't unlock anything. There was a line of text saying "Unlock with PRO" on a life-area screen. It looked interactive. It was a <Text> component with no onPress handler. The paywall never opened. A user could tap it a hundred times and get nothing.

A share card no one could reach. We'd built a synastry share card, full design, full animation. It was never mounted anywhere in the navigation tree. The only way to know it existed was to read the source.

Four different failure modes. Same root cause: the seam was missing.


Why your tools can't see this

TypeScript checks types within a file, not whether a prop gets passed between files. It knows <MirrorCard> accepts a themes prop of a certain shape. It doesn't know whether any parent ever supplies it.

Jest tests a component's behavior given a prop. Not whether that prop ever arrives in production.

ESLint catches unused imports, not unused components. A component that exports correctly but is mounted nowhere will pass every lint rule.

The only thing that catches "the chain is broken" is actually walking the chain.


The seam-tracing habit

When you see any of these on a shipping surface, treat it as a bug to investigate, not text to restyle:

  • "Coming soon"
  • "Feature locked" / "Upgrade to unlock"
  • A TODO comment in a rendered string
  • A placeholder UI that matches the app's design language perfectly

The question isn't "is this intentional?" The question is: can a user actually get what this surface promises? Walk the chain from the user's tap to the data being delivered. If any link is missing, that's the bug.

For each feature seam, I now check four things:

  1. Does the parent pass the data? Look at every call site of the component. Is the relevant prop actually supplied?
  2. Does the API response include the field? Not "does the function compute it." Does the response body carry it back to the client?
  3. Is the component actually mounted? Search your navigation tree and any conditional renders. Is there a path a user can actually reach this component?
  4. Does the CTA route somewhere real? That "Unlock with PRO" text from earlier was a <Text> with no onPress. Check every tappable element traces to an actual handler. console.log and empty onPress={() => {}} are not handlers.

This sounds obvious stated plainly. In practice, when you're refactoring UI across 40 screens, you restyle what's there and move on. The seam audit is a separate pass.


After any broad refactor: run a completeness audit

We added a step to our post-refactor checklist:

# Find any "coming soon" or placeholder text in the shipping codebase
grep -rni "coming soon\|todo\|placeholder" src/ --include="*.tsx" --include="*.ts" --include="*.jsx"
Enter fullscreen mode Exit fullscreen mode

For every hit, ask: is this intentional? If no, trace the seam.

For the highest-stakes paths (paywall CTAs, PRO feature gates, any screen a paying user specifically navigated to), we live-smoke the deployed build. Not the dev build. The deployed one. Because "code exists" in a dev environment and "feature works" in production are different claims.

The flow for a PRO gate:

  1. Log in as a subscribed user on the deployed build
  2. Navigate to the feature
  3. Try the thing the UI promises
  4. Verify you got actual content, not an empty state or a silent dead end

This takes 15 minutes for a full suite of critical paths. It catches things that would otherwise reach users and never generate an error log. Silent disappointment is the outcome.


The thing about silent failures

Errors are easy. Your monitoring catches them, your users report them, your CI catches them in tests.

Silent failures are harder. The app doesn't crash. No error is logged. The user taps a button that does nothing, or sees an empty state they assume is "just how it works," or pays for a feature they can never access. They churn. You never know why.

The seam audit exists specifically to catch that class of failure before it reaches users.

One unwired prop. That's all it took to make a paid feature invisible to every subscriber we had.


If you're building on Expo + Supabase, the seam pattern shows up most reliably at the edge function boundary. The function computes correctly. What it returns to the client is a different question. Always verify the response body, not just the function logic.

What's the dumbest seam bug you've shipped? Mine was a <Text> that looked like a button for three months.

We write about building real apps (and the things that go wrong) at astraedus.dev.

Top comments (0)