Skip to content

Commit 8bf837a

Browse files
committed
audit: add react-doctor baseline audit and test suite for web app
1 parent 6b8eba4 commit 8bf837a

2 files changed

Lines changed: 417 additions & 0 deletions

File tree

apps/web/REACT-AUDIT.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# React Doctor — Web App Baseline Audit
2+
3+
**Date:** 2026-02-21
4+
**Scope:** `apps/web/src/` — 6 React components, 5 lib modules, 3 test files
5+
**React version:** 19.2.4 | **Astro version:** 5.2.0
6+
7+
---
8+
9+
## Overall Score: 78/100
10+
11+
| Category | Score | Max | Notes |
12+
|----------|-------|-----|-------|
13+
| Hook Hygiene | 9 | 10 | All effects are external-system sync; no anti-patterns |
14+
| State Management | 8 | 10 | Minimal state; minor: URL parsing on every render |
15+
| Performance | 7 | 10 | Index keys in output, uncleaned timeouts, all `client:load` |
16+
| Accessibility | 6 | 10 | Good labels/ARIA on forms; missing on interactive widgets |
17+
| TypeScript Strictness | 8 | 10 | Strict mode; two unsafe `as unknown` casts |
18+
| Test Coverage | 6 | 10 | Utilities fully tested; zero component-level tests |
19+
| Error Handling | 9 | 10 | Proper user-facing errors; no ErrorBoundary |
20+
| Code Organization | 9 | 10 | Clean separation, co-located tests, focused components |
21+
| Security | 8 | 10 | Credentials include, key shown once; clipboard lacks error handling |
22+
| Bundle Efficiency | 8 | 10 | All dashboard components eagerly loaded (`client:load`) |
23+
24+
---
25+
26+
## Issues Found
27+
28+
### P1 — Should Fix
29+
30+
#### 1. Missing `aria-expanded` on ExecCard toggle
31+
**File:** `components/replay/ReplayViewer.tsx:61-65`
32+
**Category:** Accessibility
33+
The exec card expand/collapse button uses a chevron (``/``) but lacks `aria-expanded` to communicate state to assistive tech.
34+
35+
#### 2. Missing `aria-label` on error icon
36+
**File:** `components/replay/ReplayViewer.tsx:186`
37+
**Category:** Accessibility
38+
The lock emoji (`🔒`) used as an error icon has no `aria-label` or `role="img"`. Screen readers will announce the raw Unicode character.
39+
40+
#### 3. Missing label on invite email input
41+
**File:** `components/dashboard/OrgSettings.tsx:232`
42+
**Category:** Accessibility
43+
The invite email `<input>` has no associated `<label>` or `aria-label`. Placeholder text alone is insufficient for accessibility.
44+
45+
#### 4. Uncleaned `setTimeout` in ApiKeyManager
46+
**File:** `components/dashboard/ApiKeyManager.tsx:96`
47+
**Category:** Performance / Correctness
48+
`setTimeout(() => setCopied(false), 2000)` is never cleared. If the component unmounts within 2s, this calls `setState` on an unmounted component.
49+
50+
#### 5. Uncleaned `setTimeout` in OrgSettings
51+
**File:** `components/dashboard/OrgSettings.tsx:81`
52+
**Category:** Performance / Correctness
53+
Same pattern: `setTimeout(() => setUpdateSuccess(false), 3000)` is never cleared.
54+
55+
### P2 — Nice to Fix
56+
57+
#### 6. URL parsing on every render in VerifyOtpForm
58+
**File:** `components/auth/VerifyOtpForm.tsx:13-16`
59+
**Category:** Performance
60+
`new URLSearchParams(window.location.search)` is called on every render. Since the URL doesn't change during this component's lifetime, this should be computed once (e.g., outside the component or in a `useMemo`).
61+
62+
#### 7. Unsafe type casts in OrgSettings
63+
**File:** `components/dashboard/OrgSettings.tsx:51-53`
64+
**Category:** TypeScript Strictness
65+
Triple `as unknown as X` casts bypass TypeScript entirely. These should use BetterAuth's own types or a validated transform.
66+
67+
#### 8. Index keys for output entries
68+
**File:** `components/replay/ReplayViewer.tsx:89`
69+
**Category:** Performance
70+
Output entries use `key={i}` (array index). These are append-only and immutable after load, so impact is minimal, but a content hash or entry timestamp would be more robust.
71+
72+
#### 9. All dashboard pages use `client:load`
73+
**Files:** `pages/dashboard/*.astro`
74+
**Category:** Bundle Efficiency
75+
All three dashboard pages use `client:load` which eagerly hydrates React. `client:idle` would defer hydration until the browser is idle, improving perceived performance on slower devices.
76+
77+
### P3 — Low Priority
78+
79+
#### 10. No React ErrorBoundary
80+
**Category:** Error Handling
81+
No `ErrorBoundary` wraps any React island. An unhandled error in a React component will crash the entire island with a white screen.
82+
83+
#### 11. Empty `<th>` elements without scope
84+
**Files:** `SandboxList.tsx:121`, `ApiKeyManager.tsx:174`, `OrgSettings.tsx:205`
85+
**Category:** Accessibility
86+
Action columns have empty `<th>` elements. Adding `<th scope="col"><span class="sr-only">Actions</span></th>` would help screen readers.
87+
88+
#### 12. `mobile-dash-btn` missing `aria-expanded`
89+
**File:** `layouts/DashboardLayout.astro:44`
90+
**Category:** Accessibility
91+
The mobile menu toggle button has `aria-label` but not `aria-expanded`. State should be toggled in the click handler.
92+
93+
---
94+
95+
## What's Working Well
96+
97+
- **Zero useEffect anti-patterns.** All 5 effects are external-system sync (data fetching, focus management, redirects). No derived-state-in-effects, no chained effects, no effects-to-notify-parent.
98+
- **Proper async cancellation.** `ReplayViewer` uses a `cancelled` flag in its fetch effect cleanup — correct pattern.
99+
- **Good accessibility baseline.** Forms use `htmlFor`/`id` pairing, `role="alert"` for errors, `role="status"` for success, `aria-label` on OTP inputs.
100+
- **Clean state management.** Components use minimal `useState`. `Set<string>` for tracking in-flight mutations is a good pattern.
101+
- **Security-conscious API key handling.** Key shown once, then masked. Good UX + security.
102+
- **Utility test coverage.** All pure functions in `lib/` have thorough tests with edge cases.
103+
- **No barrel re-exports or circular dependencies.**
104+
- **ESM imports with `.js` extensions** per project convention.
105+
106+
---
107+
108+
## Typecheck Findings
109+
110+
`astro check`**0 errors, 0 warnings, 7 hints**:
111+
112+
- 2x unused `weak` variable in `BentoGrid.astro:147` and `Cta.astro:147` (canvas animation helpers)
113+
- 5x deprecated `React.FormEvent` in React 19 — should migrate to `React.FormEvent` from `react` namespace directly (affects EmailForm, VerifyOtpForm, ApiKeyManager, OrgSettings)
114+
115+
---
116+
117+
## Test Suite
118+
119+
`react-doctor.test.ts`**68 tests, all passing**
120+
121+
The test suite programmatically audits all `.tsx` components for:
122+
- useEffect anti-patterns (derived state, chained effects, notify-parent)
123+
- Raw string throws
124+
- `any` type annotations
125+
- `console.log` in production code
126+
- Form input accessibility (labels/aria-label)
127+
- Icon button accessibility
128+
- Error message `role="alert"` usage
129+
- List key patterns
130+
- Inline style counts
131+
- useCallback dependency arrays
132+
- Baseline score tracking
133+
134+
Known issues are documented as expected baseline values in the test, so regressions will cause failures and improvements can be tracked by updating the expected values.
135+
136+
---
137+
138+
## Recommendations (Prioritized)
139+
140+
1. Add `aria-expanded` to all toggle buttons (ExecCard, mobile menu)
141+
2. Add labels to all form inputs (invite email)
142+
3. Add `role="alert"` to dashboard error messages (3 components)
143+
4. Clean up `setTimeout` calls with `useEffect` return or `useRef` for timer IDs
144+
5. Add a root `ErrorBoundary` to catch rendering errors in React islands
145+
6. Move URL parsing out of render path in VerifyOtpForm
146+
7. Replace `as unknown` casts with proper BetterAuth types
147+
8. Consider `client:idle` for non-critical dashboard pages
148+
9. Add component-level tests for key user flows (OTP input, sandbox list)

0 commit comments

Comments
 (0)