Skip to content

Commit 303b129

Browse files
fix(deployments): hide empty-state during load error (F1) (#200)
On a 429/5xx the DeploymentsPage catch handler sets items=[] (honest — nothing loaded), so the "No deployments yet" create-CTA (deployments-empty) rendered at the same time as the deployments-error banner: contradictory UX that tells the user both "nothing here, create one" and "something went wrong". Gate the empty-state on `!err` so the error banner is the sole dominant signal during a load error. The genuine zero-deployments empty state (no error) is unchanged and still shows the create CTA. Found by the per-tier error-state matrix sweep (PR #199). Tests (DeploymentsPage.test.tsx): - error → error banner shown AND empty-state NOT shown (fails before fix) - 429 → retry-hint banner, no empty row (fails before fix) - genuine empty (no error, zero items) → create CTA still shown - happy-path empty has no error banner Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 861f659 commit 303b129

2 files changed

Lines changed: 52 additions & 1 deletion

File tree

src/pages/DeploymentsPage.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,50 @@ describe('DeploymentsPage — empty state', () => {
116116
expect(banner.textContent).toMatch(/Could not load deployments/)
117117
})
118118

119+
it('F1: on a load error the empty-state CTA is NOT rendered alongside the error banner', async () => {
120+
// F1 (bug-hunt, PR #199 per-tier error-state matrix): the catch handler
121+
// sets items=[], so the empty-state condition (items.length === 0) was
122+
// also true during an error — the page showed "No deployments yet"
123+
// (deployments-empty) at the same time as deployments-error. Contradictory
124+
// UX: "nothing here, create one" AND "something went wrong". The empty
125+
// state is now gated on `!err`. This test FAILS before the fix (the empty
126+
// CTA appears) and PASSES after.
127+
mockListDeployments.mockRejectedValueOnce(new Error('boom'))
128+
render(withRouter(<DeploymentsPage />))
129+
// Error banner is the dominant signal.
130+
const banner = await screen.findByTestId('deployments-error')
131+
expect(banner).toBeTruthy()
132+
// The empty / create-CTA row must NOT be present during an error.
133+
expect(screen.queryByTestId('deployments-empty')).toBeNull()
134+
expect(screen.queryByText(/No deployments yet/i)).toBeNull()
135+
})
136+
137+
it('F1: a 429 rate-limit error shows the retry hint with NO empty-state row', async () => {
138+
// Companion to the F1 guard above for the rate-limited surface: a 429
139+
// must render the retry-hint banner and suppress the empty/create CTA.
140+
const rateLimited: Error & { status?: number; retryAfter?: number } = new Error('rate limited')
141+
rateLimited.status = 429
142+
rateLimited.retryAfter = 30
143+
mockListDeployments.mockRejectedValueOnce(rateLimited)
144+
render(withRouter(<DeploymentsPage />))
145+
const banner = await screen.findByTestId('deployments-error')
146+
expect(banner.textContent).toMatch(/Too many requests/i)
147+
// No empty / create CTA during the rate-limited error.
148+
expect(screen.queryByTestId('deployments-empty')).toBeNull()
149+
})
150+
151+
it('F1: genuine empty (no error, zero items) still renders the create-CTA', async () => {
152+
// The fix must NOT suppress the happy-path empty state. With no error and
153+
// zero deployments the create-CTA must still show.
154+
mockListDeployments.mockResolvedValueOnce({ ok: true, items: [], total: 0 })
155+
render(withRouter(<DeploymentsPage />))
156+
const empty = await screen.findByTestId('deployments-empty')
157+
expect(empty).toBeTruthy()
158+
expect(empty.textContent).toMatch(/No deployments yet/i)
159+
// And no error banner in the happy path.
160+
expect(screen.queryByTestId('deployments-error')).toBeNull()
161+
})
162+
119163
it('renders a 429 rate-limit hint with the Retry-After seconds', async () => {
120164
// Regression guard for T15 P2-7: DeploymentsPage now consumes
121165
// retryHint just like TeamPage. A rejected fetch carrying status=429

src/pages/DeploymentsPage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,14 @@ export function DeploymentsPage() {
238238
<span className="skel" style={{ width: '60%', height: 18, margin: '0 auto' }} />
239239
</div>
240240
)}
241-
{!loading && items.length === 0 && (
241+
{/* F1 (bug-hunt, PR #199 error-state matrix): gate on `!err` so the
242+
"No deployments yet" create-CTA never renders alongside the
243+
deployments-error banner. On a 429/5xx the catch handler sets
244+
items=[] (honest — nothing loaded), which would otherwise satisfy
245+
items.length === 0 and show the empty CTA at the same time as the
246+
error. The error banner is the dominant signal; the genuine
247+
zero-deployments empty state (no error) still shows. */}
248+
{!loading && !err && items.length === 0 && (
242249
<div
243250
className="table-row"
244251
data-testid="deployments-empty"

0 commit comments

Comments
 (0)