# RED-test robustness: verify the RED before optimizing The C-gate of the workflow. A RED that is red for the wrong reason sends you optimizing a route that was never broken: the route becomes instant, the test stays red (or the code is contorted to satisfy a broken assertion), and the effort lands on the wrong problem. The prevention is cheap: spend a few minutes verifying the RED is trustworthy first. ## The deciding question > **Does the marker render WITHOUT the lock, as the test user?** - **No**: the test is red because the marker or page is not there for that user or environment. A marker bug, not an instant-navigation bug. Fix the marker. (This is the most common case.) - **Yes**: the marker exists and is reachable; a red under the lock is a genuine "not instant". Optimize the route. Everything below serves answering that question honestly. ## When the blocked route will not build With Cache Components, a top-level blocking read can fail the build before the test can produce a RED. Add `export const instant = false` to the target route as part of the temporary RED scaffold. It lets the known blocker build without making the navigation instant. Remove the opt-out with the fix, and include it when reverting the fix for the differential. ## Cookie and session reads are not lock probes Do not manufacture a RED with `cookies()` or a session read alone. The testing lock restricts the navigation to its shell; it does not make request cookies unavailable. Use the route's real blocking uncached data, and prefer the self-validating test variant when deferred content exists. ## The robustness checklist (all must hold) 1. **Red on baseline**: fails on the unfixed route. 2. **Right reason**: without the lock, the marker is visible on the production-build rig (never `next dev`), running as the test user, not only in the author's own logged-in session. 3. **Differential**: reverting only the fix → RED; re-applying → GREEN; nothing else moves it. 4. **Non-gameable marker**: a sync element of the static shell, never streamed data. 5. **Deterministic on a production build**: stable across N runs; never `next dev`. 6. **Discriminating both ways**: present under the lock on an instant route, absent under the lock on a blocking one. 7. **Renders for the test user**: under that user's flags, plan, role, and data. 8. **Conditional redirects accounted for**: assert at the route's real destination for that user. 9. **Real selector**: a `data-testid` on a known static-shell node, not a guessed `role`/`name`. 10. **Visible marker**: not `display:none`, off-screen, or inside a hover overlay; for lists, target `.filter({ visible: true }).first()`. 11. **Fresh build under test**: the deployment being measured contains the latest commit, not a build URL still serving the previous deploy. ## Taxonomy: red for the wrong reason Any of these makes a RED untrustworthy. None of them is "the navigation isn't instant." | Wrong reason | How it occurs | How to rule it out | | ---------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | **Selector matches nothing** | a guessed `getByRole('button', { name: 'Folder' })` | grep the component for the real accessible name; add a `data-testid` | | **Conditional redirect** | the route `redirect()`s for the test user (flag/role), so the marker page is never reached | check the page's top-level branches; assert at the real destination, or pin the flag | | **Flag / plan / role gate** | the author has the flag or plan; the test user does not | run the unlocked baseline as the test user; pin flags via the project's override mechanism | | **Empty state** | the marker only exists when data does; the CI account is empty | pick a marker present in the empty state (a layout element such as the page header), or seed data | | **Timeout / flake** | a slow API or transient infrastructure error | re-run; separate infrastructure flake from a real signal | | **Streamed marker** | the marker is behind ``, so it is never in the shell | choose a sync shell element; verify it sits outside every `` | | **Auth redirect** | unauthenticated → `/login` | confirm login succeeded before the navigation | | **Stale deployment** | the test ran against the previous build (the URL under test still serves the prior deploy) | poll the deployment for a marker from the latest commit before trusting any verdict | | **Hidden / off-screen** | the testid is on a hover-overlay or off-screen list item | put the marker on an always-visible node; `.filter({ visible: true }).first()` for lists | ## Worked cases These are illustrative failures from real optimization runs; each was red for a wrong reason, and none was an instant-navigation problem. One app's drift surface might be dominated by feature flags and plans; another's by auth state, an empty database, or locale. The taxonomy lists every wrong reason; the rig file's DRIFT list says which rows apply to your app. - **Guessed selector + empty state**: the marker was a button picked by a guessed accessible name that no element actually had, on a list page whose CI account had no rows. → checks 7, 9. Fix: a `data-testid` on a real static-shell node. - **Hidden marker**: the testid sat first on a `hidden sm:block` hover-overlay link, then on an off-screen carousel card; Playwright resolved the element but reported it hidden. → check 10. Fix: an always-visible node; for lists, `.filter({ visible: true }).first()`. ## Differential check (capture in the PR) The strongest evidence that the RED measured the property: ``` 1. on the fixed branch → GREEN 2. revert ONLY the fix (the push-down) → RED 3. re-apply → GREEN 4. confirm no other change moves it ``` Link the two runs (or include the toggle diff and results) in the PR description. A reviewer who sees the differential knows the test measures the property. ## `instant()` is not a stopwatch See: [`instant()`](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests). The test does not measure how fast a navigation is. `instant()` gates dynamic data so the content of the static shell can be asserted; the signal is presence, not speed. Under the lock, an instant route's shell is present, and a blocking route's content never commits, regardless of wait time. Therefore: - The shipped assertion is `await expect(SHELL_MARKER).toBeVisible()` under the lock. Do not add a custom timeout or a `painted` boolean. - A custom short timeout (e.g. `3000`) implies a race against a clock that does not exist. It adds nothing to the verdict and invites false REDs on an instant route whose commit lands a microtask late. - Do not use `locator.isVisible({ timeout })` as a soft wait: Playwright deprecated and ignores that timeout; the call returns immediately. - "Renders for the test user" (checks 7-9) is established at authoring time with the unlocked baseline scaffold, not by a timed assertion in the shipped test. ## `instant()` guards need no retries and no prefetch warming An `instant()` guard is deterministic. Do not configure retries on one, and do not hover-warm to help a prefetch land in time. Under the lock, the router initiates the route prefetch and awaits it before committing (even for a `prefetch={false}` link, even for a route already in the prefetch cache), so the committed shell does not depend on any prior render, hover, or menu-open prefetch. A flaky guard has a real cause: a marker that is not a sync node of the destination's shell, a flag/role/empty-state gap for the test user, or a genuinely blocking route. The fix is in the page or the marker; a retry masks the regression the guard exists to catch. The only legitimate `.hover()`/menu-open is when the trigger element itself is not in the DOM until hovered or opened. ## Silent no-op: the testing API must be exposed in the measured build `instant()` works by setting a cookie (`next-instant-navigation-testing`) that lock code inside the build reads. It does not throw when that lock code is absent; it only throws on nested calls or an unknown base URL. If the build was produced without the testing API (`experimental.exposeTestingApiInProductionBuild`), the cookie is ignored, the navigation runs normally, and the `instant()` test passes vacuously. A green `instant()` test is only meaningful if the lock engaged. Two defenses; use both: 1. **Confirm the API is exposed on the target.** Wire the flag to the platform's preview/staging condition or an explicit environment variable; the rig file records the project's spelling (SKILL.md phases 0 and A). Do not trust a pass from a build where it is not set. 2. **Make the test self-validating**: for any route with deferred content, also assert that the deferred content is gated under the lock, not only that the shell is present (`../test-template.md`, self-validating variant). If the lock did not engage, the content is already present and `toHaveCount(0)` fails. The gated half holds under the lock for both navigation types regardless of warm state: the soft-nav client lock gates dynamic-data writes, and on an initial load the server honors the cookie on the document request and suspends dynamic data. A vacuous pass is only possible with a build produced WITHOUT the testing API, which defense #1 above covers. ## Determinism and the rig - Always measure on a production build, never `next dev`; SKILL.md phase A owns this invariant and its rationale. - Run the RED several times; an intermittently red gate is not a gate. If it flakes, determine whether the cause is infrastructure (transient errors) or a real race before trusting either color.