인사이트로 돌아가기
Navigation & UX

Three Production Bugs That Started as Unreachable Features

Three feature pages in a codebase with severed navigation links to user personas.

이 글은 영어로만 제공됩니다.

The support ticket reads: “I can’t find the billing page.” The feature shipped six weeks ago. The code is in production. The route responds correctly when you paste the URL. Three customers have reported it now. The link was added to the admin sidebar but not the user sidebar. Tests logged in as admin. Code coverage: 94%. Features reachable by your users: unknown.

“Shipped but unreachable” is a distinct failure class in software quality. A feature is in the codebase, the route resolves, the page renders, and code coverage reports every line as executed. But no navigation path in the application connects the feature to the user who needs it. The gap is in the link graph, not the code graph. Navigation Coverage is the metric that catches this class of failure by measuring the percentage of features reachable through actual user navigation, per persona.

Defining the failure class: shipped but unreachable

A “shipped but unreachable” feature is in the codebase, passes all tests, registers full code coverage, and is invisible to the user who needs it. The gap is in the link graph, not the code graph.

“Shipped but unreachable” is a distinct failure class. The feature is in the codebase. The route resolves. The page renders. The code coverage report counts every line as executed. But no navigation path in the application connects the feature to the user who needs it. The link is missing, the menu item is absent, the button does not appear for the persona who should see it.

This is not the same as “broken.” A broken feature throws errors. A broken feature fails tests. An unreachable feature passes every test in the suite — because the suite reaches it by URL, the same way the developer reached it during implementation. The user, who navigates by clicking links in the application, never encounters it at all.

And this is not the same as “untested.” The route has coverage. The component renders. The API responds. The gap is not in the code graph; it is in the link graph. The test suite exercises the code. It does not exercise the path a user follows to get there.

Why code coverage does not catch this

Code coverage measures which lines execute, not whether a user can navigate to the feature that contains them. A test that calls page.goto('/billing') registers coverage for the billing component but never asks whether /billing is linked from any page the user can see.

Code coverage measures executed lines, branches, and functions. A route that renders when navigated to directly will register coverage for every line in the component. If a test calls page.goto('/billing'), the billing page’s code executes, and coverage increases. But the test never asked whether /billing is linked from any page the user can see.

This is not a theoretical gap. Broken Access Control is the number-one category in the OWASP Top 10, and a substantial fraction of access control failures are not about missing server-side checks — they are about UI paths that do not match backend permissions. The route exists, the check passes, but the navigation that should surface the route is absent for one or more user types.

Case 1: the RBAC endpoint exposed by direct URL

The pattern: an admin-only endpoint is accessible by direct URL to any authenticated user, but no non-admin UI element links to it. The route has no server-side permission check because the application’s access control is enforced at the navigation layer — if the link does not appear in the sidebar, the assumption is that the user cannot reach the route. This is the same structural assumption that CVE-2025-29927 exploited in Next.js middleware: the boundary is enforced at the navigation layer, and when that layer fails, every route behind it is exposed.

This is one of the most frequently reported categories in bug bounty programmes. HackerOne’s public disclosure database lists hundreds of authorisation bypass reports where the common thread is a route that responds successfully to any authenticated request but is only surfaced in an admin-facing UI. The researcher discovers the endpoint, proves it responds without the expected permission check, and the bounty is paid.

The testing gap: the admin-authenticated test suite navigated to the endpoint and verified it rendered correctly. The suite never navigated as a non-admin user to verify the endpoint was either (a) unreachable or (b) properly restricted at the server layer. The link was absent from the non-admin sidebar, so the suite — logging in as admin — assumed the route was tested. It was tested for admin. It was untested for everyone else. And the missing server-side check meant anyone could reach it by URL.

A persona-aware fixture would have caught this by diffing the reachable routes per persona. If the admin graph includes /admin/settings and the member graph does not, but a member can page.goto('/admin/settings') without a redirect or 403, that asymmetry is the bug.

Case 2: the billing page missing for free-tier users

The pattern: a SaaS application adds a billing management feature. The link is added to the sidebar component inside a conditional block that renders for users with an active subscription. Free-tier users — who are the primary audience for the upgrade flow — do not see the link. The route exists, the page renders, and the billing flow works perfectly when navigated to directly. But free-tier users, the users most likely to need it, cannot find it.

This is one of the most common navigation-layer failures in multi-tier SaaS applications. The feature works. The tests pass. The billing page has 100% code coverage. The bug is not in the code; it is in the conditional rendering logic of the sidebar component, which was tested only with the admin or enterprise fixture.

The support tickets accumulate slowly. Free-tier users do not report “the billing page is broken” — they report “I can’t find how to upgrade” or “where is my billing page?” The triage team checks the route, confirms it works, and marks the ticket as user error. The pattern persists for weeks before anyone realises the navigation link is simply not rendered for the persona that needs it most.

The revenue impact is direct: every free-tier user who cannot find the upgrade path is a lost conversion. The test suite says nothing because the test suite has never logged in as a free-tier user. The five-minute route audit would surface this immediately — the billing route is in the inventory but absent from the free-tier persona’s reachable links.

Case 3: the feature unreachable by keyboard

The pattern: a feature is linked from a dropdown menu that opens on mouse hover. The menu is implemented as a custom component using onMouseEnter and onMouseLeave event handlers. It has no keyboard equivalent. There is no onFocus or onKeyDown handler, no aria-expanded attribute, and no focus management when the menu opens.

Sighted mouse users reach the feature without difficulty. Keyboard users — who include screen reader users, users with motor disabilities, users who prefer keyboard navigation, and users on devices without a pointing device — cannot open the menu and therefore cannot reach the feature.

The HTTP Archive Web Almanac 2024 found that only 43% of pages use either the <main> element or role="main", and approximately 24% include skip links. These are the foundational navigation structures for keyboard users. Without them, keyboard navigation is at best difficult and at worst impossible. A hover-only menu is a microcosm of the same structural failure: the navigation path exists for one input method but not another.

Since the European Accessibility Act came into force on 28 June 2025, this is no longer just a quality issue. WCAG 2.1.1 (Keyboard) requires that all functionality be operable through a keyboard interface. A feature that is reachable by mouse but not by keyboard is a compliance failure with legal consequences in 27 EU member states.

Even Playwright’s Test Agents, introduced in v1.56 (October 2025), explore applications through the accessibility tree rather than mouse events. If the hover menu is not keyboard-accessible, the Planner agent’s exploration will miss it too — because the agent navigates the same way a keyboard user would. The feature is unreachable for the agent, just as it is for the user.

Introducing Navigation Coverage

Navigation Coverage measures the percentage of an application’s features that are reachable through actual user navigation, per persona. A route with no link from any page the user can see scores zero for that persona. When Navigation Coverage reaches 100%, every feature is reachable by every user type through normal navigation.

The three cases above share a structure: a feature exists in the codebase, passes its tests, registers code coverage, and is unreachable by one or more user types through the application’s actual navigation. Code coverage does not detect this because code coverage measures execution, not reachability. Test-pass rates do not detect this because the tests navigate by URL, not by user path.

Navigation Coverage measures something different. It measures the percentage of an application’s features that are reachable through actual user navigation, per persona. A route that exists but has no link from any page the user can see scores zero for that persona. A route that is linked, loads without error, and renders the expected content scores one. The aggregate across all routes and all personas is the Navigation Coverage score. When it reaches 100% across all personas, every feature in the application is reachable by every user type through normal navigation.

The three cases above all surface as Navigation Coverage failures: Case 1 scores zero for non-admin personas on the admin endpoint (which may be correct — but the missing server-side check means the restriction is enforced only at the UI layer). Case 2 scores zero for free-tier users on the billing page (which is unambiguously a bug). Case 3 scores zero for keyboard-only users on the hover-menu feature (which is both a bug and a compliance failure).

How to construct a navigation graph audit

Start with the lightweight approach from the five-minute route audit: enumerate your routes from the source code, enumerate the routes your tests visit, and diff the two lists. This catches the obvious gaps.

For the systematic version, build a per-persona crawl. Authenticate as each persona, start from the entry point (typically /dashboard or /home), collect every link on the page, navigate to each link, and recurse. The output is a navigation graph per persona: every route reachable from the authenticated starting point through normal link traversal.

Diff the persona graphs against each other and against the route inventory. The delta between the admin graph and the free-user graph will contain both intentional restrictions and unintentional gaps. The delta between the route inventory and any persona’s graph will contain routes that exist in the codebase but are unreachable from that persona’s navigation.

The persona-aware Playwright fixture provides the infrastructure for this. Each persona project authenticates independently, runs the same navigation sweep, and reports which routes are reachable. The diff between projects is the gap.

When the audit surfaces gaps, classify each one:

Broken link. The route should be reachable for the persona, and the navigation link is missing or broken. Fix the link. This is the billing-page case.

Intentional restriction. The route is correctly hidden from the persona — admin-only features, enterprise-only integrations, deprecated endpoints. Document the restriction as an expected gap in your test baseline so it does not resurface as a false positive.

Oversight. The route was added to the codebase but never linked from any navigation element. The developer implemented the page and assumed the link would be added elsewhere. Nobody added it. This is the most common pattern, and it accumulates with every feature release.

Security gap. The route is hidden from the persona’s navigation but accessible by direct URL without proper server-side authorisation. This is the RBAC case. It requires both a navigation fix (link the route or remove the UI reference) and a backend fix (add the authorisation check).

Building Navigation Coverage into CI

A Navigation Coverage check in CI runs after the test suite and before deployment. It authenticates as each defined persona, crawls the navigation graph from the entry point, and compares the reachable routes against the expected baseline.

A failing check means one of three things: a new route was added without a navigation link, an existing link was broken by a UI change, or a persona’s navigation graph contracted (fewer routes reachable than before). Each failure is specific — the check tells you which persona, which route, and whether the route is new or previously reachable.

This is not a replacement for the test suite. It is a complement. The test suite verifies that features work. The Navigation Coverage check verifies that users can find them.


Frequently asked questions

What is the “shipped but unreachable” failure class? A feature is “shipped but unreachable” when the code is deployed, the route resolves, and the page renders correctly when accessed by direct URL, but no navigation path in the application connects the feature to the user who needs it. The link is missing from the sidebar, the menu item does not appear for the persona, or the button requires an input method (mouse hover) that keyboard-only users cannot invoke.

Can a feature have 100% code coverage but be unreachable through the UI? Yes. Code coverage measures which lines of source code execute during tests. If a test navigates to a route by URL (page.goto('/billing')), the component renders, and every line registers as covered. But the test never checked whether /billing is linked from any page the user can see. The feature has full coverage and zero reachability.

What is Navigation Coverage and how does it differ from code coverage? Navigation Coverage measures the percentage of an application’s features that are reachable through actual user navigation, per persona. It answers a question code coverage cannot: can a user get to this feature by clicking through the application? A route that is linked, loads without error, and renders the expected content scores as reachable. A route that exists but has no link from any page the persona can see scores as unreachable.

What are common causes of unreachable features in production? The three most common patterns are RBAC misconfiguration (admin-only endpoints accessible by URL but not linked for non-admin personas), conditional rendering bugs (features linked for one subscription tier but not another), and accessibility gaps (features reachable by mouse navigation but not by keyboard). All three pass code coverage checks and typical CI pipelines.

How do I detect unreachable features before they reach production? Start with the five-minute route audit to enumerate routes and diff against test visits. For systematic detection, build persona-aware Playwright fixtures that crawl the navigation graph per persona and surface asymmetries between what each user type can reach.

여러분의 앱에서 커버리지 스캔을 실행하십시오.

Glia Quest를 스테이징 또는 프로덕션 URL에 연결해 보십시오. 첫 실행은 무료이며, 보고서는 2분 안에 도착합니다.