feat(unit-4): Dashboard — availability widget with TanStack Query
- Add @tanstack/react-query 5.101.0; wrap app with QueryClientProvider - Add AvailabilityStatus type and AvailabilityResponse to api/types.ts - Implement useAvailabilityStatus (staleTime 30s, stale-on-error preserved) - Add AvailabilityStatusBadge with green/amber/red states and stale indicator - Replace DashboardPage placeholder card with live availability widget - Add MSW availability handler; update test/utils with QueryClientProvider - 55/55 tests pass (FR-05) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,3 +60,11 @@ export interface InviteCompleteRequest {
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type AvailabilityStatus = 'Available' | 'Maintenance' | 'Unavailable';
|
||||
|
||||
export interface AvailabilityResponse {
|
||||
status: AvailabilityStatus;
|
||||
checkedAt: string; // ISO 8601
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { createElement } from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { useAvailabilityStatus } from './useAvailability';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
}
|
||||
|
||||
const AVAILABILITY_URL = `${API_BASE}/api/v1/Availability/status`;
|
||||
|
||||
describe('useAvailabilityStatus', () => {
|
||||
it('starts in loading state', () => {
|
||||
const { result } = renderHook(() => useAvailabilityStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns data on successful fetch', async () => {
|
||||
const { result } = renderHook(() => useAvailabilityStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(result.current.isError).toBe(false);
|
||||
expect(result.current.data).toMatchObject({
|
||||
status: 'Available',
|
||||
message: '',
|
||||
});
|
||||
expect(typeof result.current.data?.checkedAt).toBe('string');
|
||||
});
|
||||
|
||||
it('enters error state when the server returns 5xx', async () => {
|
||||
server.use(
|
||||
http.get(AVAILABILITY_URL, () =>
|
||||
HttpResponse.json(
|
||||
{ title: 'Internal Server Error', status: 500 },
|
||||
{ status: 500 },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAvailabilityStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(result.current.isError).toBe(true);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it('exposes a refetch function', async () => {
|
||||
const { result } = renderHook(() => useAvailabilityStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
expect(typeof result.current.refetch).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { AvailabilityResponse } from './types';
|
||||
|
||||
export function useAvailabilityStatus() {
|
||||
return useQuery<AvailabilityResponse, Error>({
|
||||
queryKey: ['availability', 'status'],
|
||||
queryFn: () => api.get<AvailabilityResponse>('/api/v1/Availability/status'),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user