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:
@@ -0,0 +1,70 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { renderWithProviders } from '@/test/utils';
|
||||
import { AvailabilityStatusBadge } from './AvailabilityStatusBadge';
|
||||
|
||||
describe('AvailabilityStatusBadge', () => {
|
||||
it('renders Available status with green styling', () => {
|
||||
renderWithProviders(<AvailabilityStatusBadge status="Available" />);
|
||||
|
||||
const label = screen.getByTestId('availability-status-label');
|
||||
expect(label).toHaveTextContent('Available');
|
||||
|
||||
const badge = screen.getByTestId('availability-badge');
|
||||
expect(badge.querySelector('.bg-green-100')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders Maintenance status with amber styling', () => {
|
||||
renderWithProviders(<AvailabilityStatusBadge status="Maintenance" />);
|
||||
|
||||
const label = screen.getByTestId('availability-status-label');
|
||||
expect(label).toHaveTextContent('Maintenance');
|
||||
|
||||
const badge = screen.getByTestId('availability-badge');
|
||||
expect(badge.querySelector('.bg-amber-100')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders Unavailable status with red styling', () => {
|
||||
renderWithProviders(<AvailabilityStatusBadge status="Unavailable" />);
|
||||
|
||||
const label = screen.getByTestId('availability-status-label');
|
||||
expect(label).toHaveTextContent('Unavailable');
|
||||
|
||||
const badge = screen.getByTestId('availability-badge');
|
||||
expect(badge.querySelector('.bg-red-100')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows message subtitle when message is non-empty', () => {
|
||||
renderWithProviders(
|
||||
<AvailabilityStatusBadge status="Maintenance" message="Scheduled downtime until 18:00" />,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('availability-message')).toHaveTextContent(
|
||||
'Scheduled downtime until 18:00',
|
||||
);
|
||||
});
|
||||
|
||||
it('hides message element when message is empty string', () => {
|
||||
renderWithProviders(<AvailabilityStatusBadge status="Available" message="" />);
|
||||
|
||||
expect(screen.queryByTestId('availability-message')).toBeNull();
|
||||
});
|
||||
|
||||
it('hides message element when message prop is omitted', () => {
|
||||
renderWithProviders(<AvailabilityStatusBadge status="Available" />);
|
||||
|
||||
expect(screen.queryByTestId('availability-message')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows stale indicator when stale={true}', () => {
|
||||
renderWithProviders(<AvailabilityStatusBadge status="Available" stale={true} />);
|
||||
|
||||
expect(screen.getByTestId('availability-stale-indicator')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides stale indicator when stale is omitted (default false)', () => {
|
||||
renderWithProviders(<AvailabilityStatusBadge status="Available" />);
|
||||
|
||||
expect(screen.queryByTestId('availability-stale-indicator')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { AlertTriangle, CheckCircle, Clock, XCircle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AvailabilityStatus } from '@/api/types';
|
||||
|
||||
interface AvailabilityStatusBadgeProps {
|
||||
status: AvailabilityStatus;
|
||||
message?: string;
|
||||
stale?: boolean;
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<
|
||||
AvailabilityStatus,
|
||||
{ labelKey: string; colorClass: string; Icon: React.ComponentType<{ className?: string }> }
|
||||
> = {
|
||||
Available: {
|
||||
labelKey: 'availability.available',
|
||||
colorClass:
|
||||
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
Icon: CheckCircle,
|
||||
},
|
||||
Maintenance: {
|
||||
labelKey: 'availability.maintenance',
|
||||
colorClass:
|
||||
'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
|
||||
Icon: AlertTriangle,
|
||||
},
|
||||
Unavailable: {
|
||||
labelKey: 'availability.unavailable',
|
||||
colorClass:
|
||||
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
Icon: XCircle,
|
||||
},
|
||||
};
|
||||
|
||||
export function AvailabilityStatusBadge({
|
||||
status,
|
||||
message,
|
||||
stale = false,
|
||||
}: AvailabilityStatusBadgeProps) {
|
||||
const { t } = useTranslation();
|
||||
const { labelKey, colorClass, Icon } = STATUS_CONFIG[status];
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="availability-badge"
|
||||
className={stale ? 'rounded-lg border-2 border-dashed border-amber-400 p-3' : undefined}
|
||||
>
|
||||
<div className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm font-medium ${colorClass}`}>
|
||||
<Icon className="size-4" />
|
||||
<span data-testid="availability-status-label">{t(labelKey)}</span>
|
||||
</div>
|
||||
|
||||
{message && message.length > 0 && (
|
||||
<p
|
||||
data-testid="availability-message"
|
||||
className="mt-2 text-sm text-muted-foreground"
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{stale && (
|
||||
<div
|
||||
data-testid="availability-stale-indicator"
|
||||
className="mt-2 flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400"
|
||||
>
|
||||
<Clock className="size-3" />
|
||||
<span>{t('availability.staleLabel')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user