646 lines
21 KiB
Markdown
646 lines
21 KiB
Markdown
# Design Document: Signal Dashboard
|
||
|
||
## Overview
|
||
|
||
Signal Dashboard is a React 18 + TypeScript SPA that consumes the existing Stock Data Backend REST API (`/api/v1/`). It provides authenticated users with views for watchlist monitoring, per-ticker analysis, trade setup scanning, composite-score rankings, and admin management.
|
||
|
||
The frontend lives in `frontend/` within the existing project root. Vite builds static assets to `frontend/dist/`, which Nginx serves on `signal.thiessen.io`. API requests to `/api/v1/` are proxied to the FastAPI backend — no CORS needed.
|
||
|
||
### Key Technical Decisions
|
||
|
||
| Decision | Choice | Rationale |
|
||
|---|---|---|
|
||
| Build tool | Vite 5 | Fast HMR, native TS/React support, small output |
|
||
| Routing | React Router v6 | Standard, supports layout routes and guards |
|
||
| Server state | TanStack Query v5 | Caching, deduplication, background refetch |
|
||
| Client state | Zustand | Minimal auth store, no boilerplate |
|
||
| Styling | Tailwind CSS v3 | Utility-first, dark mode built-in, small bundle |
|
||
| Charts | Recharts | React-native charting, composable, lightweight |
|
||
| HTTP | Axios | Interceptors for auth/envelope unwrapping |
|
||
| Testing | Vitest + React Testing Library + fast-check | Vite-native test runner, property-based testing |
|
||
|
||
## Architecture
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph Browser
|
||
Router[React Router]
|
||
Pages[Page Components]
|
||
Hooks[TanStack Query Hooks]
|
||
Store[Zustand Auth Store]
|
||
API[API Client - Axios]
|
||
end
|
||
|
||
Router --> Pages
|
||
Pages --> Hooks
|
||
Hooks --> API
|
||
API --> Store
|
||
Store --> API
|
||
|
||
subgraph Server
|
||
Nginx[Nginx - static files + proxy]
|
||
Backend[FastAPI Backend]
|
||
end
|
||
|
||
API -->|/api/v1/*| Nginx
|
||
Nginx -->|proxy_pass| Backend
|
||
Nginx -->|static| Browser
|
||
```
|
||
|
||
### Request Flow
|
||
|
||
1. Component mounts → calls a TanStack Query hook (e.g., `useWatchlist()`)
|
||
2. Hook calls an API client function (e.g., `api.watchlist.list()`)
|
||
3. Axios sends request with JWT Bearer header from Zustand store
|
||
4. Axios response interceptor unwraps `{ status, data, error }` envelope
|
||
5. On 401 → Zustand clears token, React Router redirects to `/login`
|
||
6. TanStack Query caches the result, component renders data
|
||
|
||
### Directory Structure
|
||
|
||
```
|
||
frontend/
|
||
├── index.html
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── vite.config.ts
|
||
├── tailwind.config.ts
|
||
├── postcss.config.js
|
||
├── src/
|
||
│ ├── main.tsx # App entry, providers
|
||
│ ├── App.tsx # Router + layout
|
||
│ ├── api/
|
||
│ │ ├── client.ts # Axios instance, interceptors
|
||
│ │ ├── auth.ts # login, register
|
||
│ │ ├── watchlist.ts # watchlist CRUD
|
||
│ │ ├── tickers.ts # ticker CRUD
|
||
│ │ ├── scores.ts # scores, rankings, weights
|
||
│ │ ├── trades.ts # trade setups
|
||
│ │ ├── ohlcv.ts # OHLCV data
|
||
│ │ ├── indicators.ts # technical indicators
|
||
│ │ ├── sr-levels.ts # support/resistance
|
||
│ │ ├── sentiment.ts # sentiment data
|
||
│ │ ├── fundamentals.ts # fundamental data
|
||
│ │ ├── ingestion.ts # manual data fetch
|
||
│ │ ├── admin.ts # admin endpoints
|
||
│ │ └── health.ts # health check
|
||
│ ├── hooks/
|
||
│ │ ├── useAuth.ts # login/register/logout mutations
|
||
│ │ ├── useWatchlist.ts # watchlist queries + mutations
|
||
│ │ ├── useTickers.ts # ticker queries + mutations
|
||
│ │ ├── useScores.ts # scores, rankings queries
|
||
│ │ ├── useTrades.ts # trade setup queries
|
||
│ │ ├── useTickerDetail.ts # parallel queries for detail view
|
||
│ │ └── useAdmin.ts # admin queries + mutations
|
||
│ ├── stores/
|
||
│ │ └── authStore.ts # Zustand: token, user, role
|
||
│ ├── pages/
|
||
│ │ ├── LoginPage.tsx
|
||
│ │ ├── RegisterPage.tsx
|
||
│ │ ├── WatchlistPage.tsx
|
||
│ │ ├── TickerDetailPage.tsx
|
||
│ │ ├── ScannerPage.tsx
|
||
│ │ ├── RankingsPage.tsx
|
||
│ │ └── AdminPage.tsx
|
||
│ ├── components/
|
||
│ │ ├── layout/
|
||
│ │ │ ├── AppShell.tsx # Sidebar + main content
|
||
│ │ │ ├── Sidebar.tsx
|
||
│ │ │ └── MobileNav.tsx
|
||
│ │ ├── auth/
|
||
│ │ │ └── ProtectedRoute.tsx
|
||
│ │ ├── charts/
|
||
│ │ │ └── CandlestickChart.tsx
|
||
│ │ ├── ui/
|
||
│ │ │ ├── ScoreCard.tsx
|
||
│ │ │ ├── Toast.tsx
|
||
│ │ │ ├── Skeleton.tsx
|
||
│ │ │ ├── Badge.tsx
|
||
│ │ │ └── ConfirmDialog.tsx
|
||
│ │ ├── watchlist/
|
||
│ │ │ ├── WatchlistTable.tsx
|
||
│ │ │ └── AddTickerForm.tsx
|
||
│ │ ├── scanner/
|
||
│ │ │ └── TradeTable.tsx
|
||
│ │ ├── rankings/
|
||
│ │ │ ├── RankingsTable.tsx
|
||
│ │ │ └── WeightsForm.tsx
|
||
│ │ ├── ticker/
|
||
│ │ │ ├── SentimentPanel.tsx
|
||
│ │ │ ├── FundamentalsPanel.tsx
|
||
│ │ │ ├── IndicatorSelector.tsx
|
||
│ │ │ └── SROverlay.tsx
|
||
│ │ └── admin/
|
||
│ │ ├── UserTable.tsx
|
||
│ │ ├── SettingsForm.tsx
|
||
│ │ ├── JobControls.tsx
|
||
│ │ └── DataCleanup.tsx
|
||
│ ├── lib/
|
||
│ │ ├── format.ts # Number/date formatting utilities
|
||
│ │ └── types.ts # Shared TypeScript interfaces
|
||
│ └── styles/
|
||
│ └── globals.css # Tailwind directives + custom vars
|
||
└── tests/
|
||
├── unit/
|
||
└── property/
|
||
```
|
||
|
||
## Components and Interfaces
|
||
|
||
### API Client (`src/api/client.ts`)
|
||
|
||
Central Axios instance with interceptors:
|
||
|
||
```typescript
|
||
// Axios instance configuration
|
||
const apiClient = axios.create({
|
||
baseURL: '/api/v1/',
|
||
timeout: 30_000,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
|
||
// Request interceptor: attach JWT
|
||
apiClient.interceptors.request.use((config) => {
|
||
const token = useAuthStore.getState().token;
|
||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||
return config;
|
||
});
|
||
|
||
// Response interceptor: unwrap envelope, handle 401
|
||
apiClient.interceptors.response.use(
|
||
(response) => {
|
||
const envelope = response.data as APIEnvelope;
|
||
if (envelope.status === 'error') throw new ApiError(envelope.error);
|
||
return envelope.data;
|
||
},
|
||
(error) => {
|
||
if (error.response?.status === 401) {
|
||
useAuthStore.getState().logout();
|
||
}
|
||
const msg = error.response?.data?.error ?? error.message ?? 'Network error';
|
||
throw new ApiError(msg);
|
||
}
|
||
);
|
||
```
|
||
|
||
### Auth Store (`src/stores/authStore.ts`)
|
||
|
||
```typescript
|
||
interface AuthState {
|
||
token: string | null;
|
||
username: string | null;
|
||
role: 'admin' | 'user' | null;
|
||
login: (token: string) => void;
|
||
logout: () => void;
|
||
}
|
||
```
|
||
|
||
- `login()` decodes the JWT payload to extract `sub` (username) and `role`, stores token in `localStorage`
|
||
- `logout()` clears token from state and `localStorage`, TanStack Query cache is cleared on logout
|
||
|
||
### Protected Route (`src/components/auth/ProtectedRoute.tsx`)
|
||
|
||
```typescript
|
||
// Wraps routes that require authentication
|
||
// Props: requireAdmin?: boolean
|
||
// If no token → redirect to /login
|
||
// If requireAdmin && role !== 'admin' → redirect to /watchlist
|
||
```
|
||
|
||
### Router Layout
|
||
|
||
```typescript
|
||
// Route structure
|
||
<Routes>
|
||
<Route path="/login" element={<LoginPage />} />
|
||
<Route path="/register" element={<RegisterPage />} />
|
||
<Route element={<ProtectedRoute />}>
|
||
<Route element={<AppShell />}>
|
||
<Route path="/" element={<Navigate to="/watchlist" />} />
|
||
<Route path="/watchlist" element={<WatchlistPage />} />
|
||
<Route path="/ticker/:symbol" element={<TickerDetailPage />} />
|
||
<Route path="/scanner" element={<ScannerPage />} />
|
||
<Route path="/rankings" element={<RankingsPage />} />
|
||
<Route element={<ProtectedRoute requireAdmin />}>
|
||
<Route path="/admin" element={<AdminPage />} />
|
||
</Route>
|
||
</Route>
|
||
</Route>
|
||
</Routes>
|
||
```
|
||
|
||
### TanStack Query Hooks Pattern
|
||
|
||
Each domain has a hook file that exports query/mutation hooks:
|
||
|
||
```typescript
|
||
// Example: useWatchlist.ts
|
||
export function useWatchlist() {
|
||
return useQuery({
|
||
queryKey: ['watchlist'],
|
||
queryFn: () => api.watchlist.list(),
|
||
});
|
||
}
|
||
|
||
export function useAddToWatchlist() {
|
||
const qc = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: (symbol: string) => api.watchlist.add(symbol),
|
||
onSuccess: () => qc.invalidateQueries({ queryKey: ['watchlist'] }),
|
||
});
|
||
}
|
||
```
|
||
|
||
### Key UI Components
|
||
|
||
**ScoreCard**: Displays composite score with a colored ring/bar (green > 70, yellow 40-70, red < 40) and expandable dimension breakdown.
|
||
|
||
**CandlestickChart**: Recharts `ComposedChart` with custom `Bar` shapes for OHLCV candles. S/R levels rendered as `ReferenceLine` components with color coding (green = support, red = resistance).
|
||
|
||
**Toast System**: Lightweight toast using React context + portal. Auto-dismiss after 4 seconds. Error toasts in red, success in green.
|
||
|
||
**Skeleton**: Tailwind `animate-pulse` placeholder blocks matching the shape of cards/tables during loading states.
|
||
|
||
### Formatting Utilities (`src/lib/format.ts`)
|
||
|
||
```typescript
|
||
formatPrice(n: number): string // "1,234.56"
|
||
formatPercent(n: number): string // "12.34%"
|
||
formatLargeNumber(n: number): string // "1.23B", "456.7M", "12.3K"
|
||
formatDate(d: string): string // "Jan 15, 2025"
|
||
formatDateTime(d: string): string // "Jan 15, 2025 2:30 PM"
|
||
```
|
||
|
||
## Data Models
|
||
|
||
### TypeScript Interfaces (`src/lib/types.ts`)
|
||
|
||
```typescript
|
||
// API envelope (before unwrapping)
|
||
interface APIEnvelope<T = unknown> {
|
||
status: 'success' | 'error';
|
||
data: T | null;
|
||
error: string | null;
|
||
}
|
||
|
||
// Auth
|
||
interface TokenResponse {
|
||
access_token: string;
|
||
token_type: string;
|
||
}
|
||
|
||
// Watchlist
|
||
interface WatchlistEntry {
|
||
symbol: string;
|
||
entry_type: 'auto' | 'manual';
|
||
composite_score: number | null;
|
||
dimensions: DimensionScore[];
|
||
rr_ratio: number | null;
|
||
rr_direction: string | null;
|
||
sr_levels: SRLevelSummary[];
|
||
added_at: string;
|
||
}
|
||
|
||
interface DimensionScore {
|
||
dimension: string;
|
||
score: number;
|
||
}
|
||
|
||
interface SRLevelSummary {
|
||
price_level: number;
|
||
type: 'support' | 'resistance';
|
||
strength: number;
|
||
}
|
||
|
||
// OHLCV
|
||
interface OHLCVBar {
|
||
id: number;
|
||
ticker_id: number;
|
||
date: string;
|
||
open: number;
|
||
high: number;
|
||
low: number;
|
||
close: number;
|
||
volume: number;
|
||
created_at: string;
|
||
}
|
||
|
||
// Scores
|
||
interface ScoreResponse {
|
||
symbol: string;
|
||
composite_score: number | null;
|
||
composite_stale: boolean;
|
||
weights: Record<string, number>;
|
||
dimensions: DimensionScoreDetail[];
|
||
missing_dimensions: string[];
|
||
computed_at: string | null;
|
||
}
|
||
|
||
interface DimensionScoreDetail {
|
||
dimension: string;
|
||
score: number;
|
||
is_stale: boolean;
|
||
computed_at: string | null;
|
||
}
|
||
|
||
interface RankingEntry {
|
||
symbol: string;
|
||
composite_score: number;
|
||
dimensions: DimensionScoreDetail[];
|
||
}
|
||
|
||
interface RankingsResponse {
|
||
rankings: RankingEntry[];
|
||
weights: Record<string, number>;
|
||
}
|
||
|
||
// Trade Setups
|
||
interface TradeSetup {
|
||
id: number;
|
||
symbol: string;
|
||
direction: string;
|
||
entry_price: number;
|
||
stop_loss: number;
|
||
target: number;
|
||
rr_ratio: number;
|
||
composite_score: number;
|
||
detected_at: string;
|
||
}
|
||
|
||
// S/R Levels
|
||
interface SRLevel {
|
||
id: number;
|
||
price_level: number;
|
||
type: 'support' | 'resistance';
|
||
strength: number;
|
||
detection_method: string;
|
||
created_at: string;
|
||
}
|
||
|
||
interface SRLevelResponse {
|
||
symbol: string;
|
||
levels: SRLevel[];
|
||
count: number;
|
||
}
|
||
|
||
// Sentiment
|
||
interface SentimentScore {
|
||
id: number;
|
||
classification: 'bullish' | 'bearish' | 'neutral';
|
||
confidence: number;
|
||
source: string;
|
||
timestamp: string;
|
||
}
|
||
|
||
interface SentimentResponse {
|
||
symbol: string;
|
||
scores: SentimentScore[];
|
||
count: number;
|
||
dimension_score: number | null;
|
||
lookback_hours: number;
|
||
}
|
||
|
||
// Fundamentals
|
||
interface FundamentalResponse {
|
||
symbol: string;
|
||
pe_ratio: number | null;
|
||
revenue_growth: number | null;
|
||
earnings_surprise: number | null;
|
||
market_cap: number | null;
|
||
fetched_at: string | null;
|
||
}
|
||
|
||
// Indicators
|
||
interface IndicatorResult {
|
||
indicator_type: string;
|
||
values: Record<string, unknown>;
|
||
score: number;
|
||
bars_used: number;
|
||
}
|
||
|
||
interface EMACrossResult {
|
||
short_ema: number;
|
||
long_ema: number;
|
||
short_period: number;
|
||
long_period: number;
|
||
signal: 'bullish' | 'bearish' | 'neutral';
|
||
}
|
||
|
||
// Tickers
|
||
interface Ticker {
|
||
id: number;
|
||
symbol: string;
|
||
created_at: string;
|
||
}
|
||
|
||
// Admin
|
||
interface AdminUser {
|
||
id: number;
|
||
username: string;
|
||
role: string;
|
||
has_access: boolean;
|
||
created_at: string | null;
|
||
updated_at: string | null;
|
||
}
|
||
|
||
interface SystemSetting {
|
||
key: string;
|
||
value: string;
|
||
updated_at: string | null;
|
||
}
|
||
```
|
||
|
||
|
||
## Correctness Properties
|
||
|
||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||
|
||
### Property 1: Token storage round-trip
|
||
|
||
*For any* valid JWT token string, storing it via `authStore.login(token)` and then reading `authStore.token` and `localStorage.getItem('token')` should both return the original token string.
|
||
|
||
**Validates: Requirements 1.1, 1.6**
|
||
|
||
### Property 2: Bearer token attachment
|
||
|
||
*For any* non-null token in the auth store, every request made through the API client should include an `Authorization` header with value `Bearer {token}`.
|
||
|
||
**Validates: Requirements 1.3, 12.3**
|
||
|
||
### Property 3: Registration form validation
|
||
|
||
*For any* username string shorter than 1 character or password string shorter than 6 characters, the registration form should reject submission. *For any* username of length >= 1 and password of length >= 6, the form should allow submission.
|
||
|
||
**Validates: Requirements 1.2**
|
||
|
||
### Property 4: Route protection based on auth state
|
||
|
||
*For any* protected route path, if no token exists in the auth store, navigation should redirect to `/login`. If a valid token exists, navigation should render the protected component.
|
||
|
||
**Validates: Requirements 2.1, 2.2**
|
||
|
||
### Property 5: API envelope unwrapping
|
||
|
||
*For any* API response with `status: "success"`, the API client should return the `data` field. *For any* API response with `status: "error"`, the API client should throw an error containing the `error` field message.
|
||
|
||
**Validates: Requirements 12.2**
|
||
|
||
### Property 6: Watchlist entry rendering completeness
|
||
|
||
*For any* watchlist entry, the rendered output should contain the symbol, entry type (with a visual badge distinguishing "auto" from "manual"), composite score, dimension scores, R:R ratio, R:R direction, and S/R levels.
|
||
|
||
**Validates: Requirements 3.2, 3.7**
|
||
|
||
### Property 7: Symbol click navigation
|
||
|
||
*For any* symbol displayed in the watchlist table, scanner table, or rankings table, clicking that symbol should trigger navigation to `/ticker/{symbol}`.
|
||
|
||
**Validates: Requirements 3.6, 5.6, 6.4**
|
||
|
||
### Property 8: Score card rendering
|
||
|
||
*For any* score response with a composite score and dimension scores, the ScoreCard component should render the composite score value and one entry per dimension with its name and score.
|
||
|
||
**Validates: Requirements 4.4**
|
||
|
||
### Property 9: Sentiment panel rendering
|
||
|
||
*For any* sentiment response, the rendered SentimentPanel should display the classification, confidence value, and dimension score.
|
||
|
||
**Validates: Requirements 4.5**
|
||
|
||
### Property 10: Fundamentals panel rendering
|
||
|
||
*For any* fundamentals response, the rendered FundamentalsPanel should display P/E ratio, revenue growth, earnings surprise, and market cap (or a placeholder for null values).
|
||
|
||
**Validates: Requirements 4.6**
|
||
|
||
### Property 11: Trade setup rendering
|
||
|
||
*For any* trade setup, the rendered table row should contain the symbol, direction, entry price, stop loss, target, R:R ratio, composite score, and detection timestamp.
|
||
|
||
**Validates: Requirements 5.2**
|
||
|
||
### Property 12: Scanner filtering
|
||
|
||
*For any* list of trade setups, minimum R:R filter value, and direction filter selection: all displayed setups should have `rr_ratio >= minRR` and (if direction is not "both") `direction === selectedDirection`.
|
||
|
||
**Validates: Requirements 5.3, 5.4**
|
||
|
||
### Property 13: Scanner sorting
|
||
|
||
*For any* list of trade setups and a selected sort column, the displayed rows should be ordered by that column's values (ascending or descending based on sort direction).
|
||
|
||
**Validates: Requirements 5.5**
|
||
|
||
### Property 14: Rankings display order
|
||
|
||
*For any* rankings response, the rendered list should display entries in descending order by composite score, with each entry showing rank position, symbol, composite score, and all dimension scores.
|
||
|
||
**Validates: Requirements 6.1, 6.2**
|
||
|
||
### Property 15: Admin user table rendering
|
||
|
||
*For any* admin user record, the rendered table row should contain the username, role, and access status.
|
||
|
||
**Validates: Requirements 7.2**
|
||
|
||
### Property 16: Number formatting
|
||
|
||
*For any* finite number, `formatPrice` should produce a string with exactly 2 decimal places. `formatPercent` should produce a string ending with `%`. `formatLargeNumber` should produce a string with an appropriate suffix (`K` for thousands, `M` for millions, `B` for billions) for values >= 1000, and no suffix for smaller values.
|
||
|
||
**Validates: Requirements 13.4**
|
||
|
||
### Property 17: Weights form rendering
|
||
|
||
*For any* weights map (dimension name → number), the WeightsForm should render one labeled numeric input per dimension key.
|
||
|
||
**Validates: Requirements 11.1**
|
||
|
||
## Error Handling
|
||
|
||
### API Client Error Strategy
|
||
|
||
All errors flow through the Axios response interceptor and are surfaced via the Toast system:
|
||
|
||
| Error Type | Detection | Behavior |
|
||
|---|---|---|
|
||
| 401 Unauthorized | `error.response.status === 401` | Clear auth store, redirect to `/login` |
|
||
| API error envelope | `envelope.status === 'error'` | Throw `ApiError` with `envelope.error` message |
|
||
| Network error | No `error.response` | Throw `ApiError` with "Network error — check your connection" |
|
||
| Timeout | Axios timeout (30s) | Throw `ApiError` with "Request timed out" |
|
||
| Unknown | Catch-all | Throw `ApiError` with `error.message` fallback |
|
||
|
||
### Component-Level Error Handling
|
||
|
||
- **TanStack Query `onError`**: Each mutation hook passes errors to the toast system
|
||
- **Query error states**: Components check `isError` and render inline error messages
|
||
- **Ticker Detail partial failure**: Each data section (scores, sentiment, fundamentals, S/R, OHLCV) is an independent query. If one fails, the others still render. Failed sections show an inline error with a retry button.
|
||
- **Form validation**: Client-side validation before API calls (username length, password length, numeric inputs). Invalid submissions are blocked with inline field errors.
|
||
|
||
### Toast System
|
||
|
||
```typescript
|
||
type ToastType = 'success' | 'error' | 'info';
|
||
|
||
interface Toast {
|
||
id: string;
|
||
type: ToastType;
|
||
message: string;
|
||
}
|
||
|
||
// Auto-dismiss after 4 seconds
|
||
// Max 3 toasts visible at once (oldest dismissed first)
|
||
// Error toasts: red accent, Success: green accent, Info: blue accent
|
||
```
|
||
|
||
## Testing Strategy
|
||
|
||
### Testing Stack
|
||
|
||
| Tool | Purpose |
|
||
|---|---|
|
||
| Vitest | Test runner (Vite-native, fast) |
|
||
| React Testing Library | Component rendering + DOM queries |
|
||
| fast-check | Property-based testing |
|
||
| MSW (Mock Service Worker) | API mocking for integration tests |
|
||
|
||
### Unit Tests
|
||
|
||
Unit tests cover specific examples, edge cases, and integration points:
|
||
|
||
- **Auth flow**: Login stores token, logout clears token, 401 triggers logout
|
||
- **API client**: Envelope unwrapping for success/error, timeout config, Bearer header
|
||
- **Routing**: Unauthenticated redirect, admin-only route guard, non-admin redirect
|
||
- **Component rendering**: Each page renders with mock data, loading skeletons appear, error states display
|
||
- **Form validation**: Empty username rejected, short password rejected, valid inputs accepted
|
||
- **Confirmation dialog**: Delete ticker shows confirm before API call
|
||
- **Partial failure**: Ticker detail renders available sections when one query fails
|
||
|
||
### Property-Based Tests
|
||
|
||
Each correctness property maps to a single `fast-check` property test with minimum 100 iterations. Tests are tagged with the property reference:
|
||
|
||
```typescript
|
||
// Feature: signal-dashboard, Property 16: Number formatting
|
||
test.prop([fc.float({ min: -1e15, max: 1e15, noNaN: true })], (n) => {
|
||
const result = formatPrice(n);
|
||
expect(result).toMatch(/\.\d{2}$/);
|
||
});
|
||
```
|
||
|
||
Property tests focus on:
|
||
- **Pure functions**: `format.ts` utilities (Property 16)
|
||
- **Store logic**: Auth store token round-trip (Property 1)
|
||
- **API client interceptors**: Envelope unwrapping (Property 5), Bearer attachment (Property 2)
|
||
- **Filtering/sorting logic**: Scanner filter functions (Properties 12, 13)
|
||
- **Component rendering**: Given generated data, components render required fields (Properties 6, 8–11, 14, 15, 17)
|
||
- **Routing guards**: Protected route behavior based on auth state (Property 4)
|
||
|
||
### Test Configuration
|
||
|
||
- Vitest config in `frontend/vitest.config.ts` with jsdom environment
|
||
- `fast-check` configured with `{ numRuns: 100 }` minimum per property
|
||
- MSW handlers for all API endpoints used in integration tests
|
||
- Each property test tagged: `Feature: signal-dashboard, Property {N}: {title}`
|