A test that catches the bug your feature tests can't see
Source: Dev.to
There’s a class of bug that’s maddening: it passes every test you have, then crashes in the user’s face. I hit one in the admin UI of laravel-config-sso today, and the real fix wasn’t changing an icon name — it was writing a test that could see the bug in the first place. The admin UI uses Flux. Flux resolves icons through , and it throws for a name that doesn’t exist: Flux component [icon.ellipsis] does not exist.
It’s an easy mistake. Flux ships Heroicons, not Lucide. So your Lucide reflexes lie to you:
You type (Lucide) Flux wants (Heroicon)
ellipsis ellipsis-horizontal
trash-2 trash
eye-off eye-slash
Here’s the interesting part. I had a feature test that hits the admin route and asserts 200. Green. But the real UI crashes. How? Because in the headless test harness, Flux renders icons as no-ops. No real boots, so the icon name never gets resolved. The crash only surfaces under a full boot (testbench serve) — exactly where your automated tests don’t go. Analogy: it’s like a spell-checker that only runs when you print the document, not while you type. Your tests type away happily. The crash waits at the printer. Instead of relying on runtime, I wrote a Pest test that reads the Blade view, extracts every icon name (static and inside dynamic expressions), and asserts Flux actually ships a stub for each one: $fluxIconStubs = base_path(‘vendor/livewire/flux/stubs/resources/views/flux/icon’);
it(‘only references Flux icons that exist’, function () use ($fluxIconStubs) { expect(is_dir($fluxIconStubs))->toBeTrue(“Flux icon stubs not found”);
$view = file_get_contents(__DIR__.'/../../resources/views/livewire/sso-providers.blade.php');
// Static `icon="name"` plus quoted tokens inside dynamic
// `icon="{{ $cond ? 'eye-slash' : 'eye' }}"` expressions
preg_match_all('/icon="([a-z][a-z0-9-]*)"/', $view, $static);
preg_match_all('/icon="\{\{(.+?)\}\}"/', $view, $dynamic);
$names = $static[1];
foreach ($dynamic[1] as $expression) {
preg_match_all("/'([a-z][a-z0-9-]*)'/", $expression, $tokens);
$names = array_merge($names, $tokens[1]);
}
$names = array_values(array_unique($names));
expect($names)->not->toBeEmpty();
foreach ($names as $name) {
expect(is_file("{$fluxIconStubs}/{$name}.blade.php"))
->toBeTrue("Flux has no icon [{$name}] — use a valid Heroicon name (Flux ships Heroicons, not Lucide).");
}
});
What I like about this test: It runs against the source of truth. Flux’s icon registry is a folder of stubs in vendor/. The test checks directly against that — not a hardcoded list that goes stale. It handles dynamic icons. Toggles like eye / eye-slash are the ones that usually slip through. The second regex catches quoted tokens inside Blade expressions. The failure message teaches. When it breaks, it tells you the real reason: “Flux ships Heroicons, not Lucide.” Future-me will be grateful. That payoff was immediate: the very same Flux free-vs-Pro icon trap bit a sibling package the same day (a webhook/ellipsis/list set that only exists in Flux Pro). A guard test like this turns a “crashes in production” into a “fails in CI” — which is exactly where you want it. Not every typo deserves a test. This pattern shines when your runtime lies to you during tests — where a component becomes a no-op, where an adapter is mocked out, where the environment differs from production. There, a static test that reads the artifact (a Blade view, a config file, a migration) can catch what a dynamic test can’t. The rule I keep: if a failure only appears under a full boot but your tests run headless, don’t chase the full boot in CI. Catch the thing earlier with a static check over the source files. It’s cheaper, faster, and it never renders a no-op.