Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions frontend/__tests__/vaccination-proof-generator.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { render, screen } from '@testing-library/react';
import { VaccinationProofGenerator } from '../src/components/vaccination-proof-generator';
import { AccessibilityProvider } from '../src/contexts/AccessibilityContext';

const MOCK_CREDENTIALS = [
{ id: 'cred-1', vaccineType: 'COVID-19 (Pfizer)', verificationStatus: true, vaccinationDate: '2026-01-15' },
{ id: 'cred-2', vaccineType: 'Influenza 2025', verificationStatus: false, vaccinationDate: '2025-09-20' },
];

function renderGenerator(credentials = MOCK_CREDENTIALS) {
return render(
<AccessibilityProvider>
<VaccinationProofGenerator walletAddress="GABCDEF123456..." credentials={credentials} />
</AccessibilityProvider>
);
}

describe('VaccinationProofGenerator', () => {
it('renders section heading', () => {
renderGenerator();
expect(screen.getByText('Vaccination Proof Generator')).toBeInTheDocument();
});

it('renders credential selection dropdown', () => {
renderGenerator();
expect(screen.getByLabelText('Select Vaccination Credential')).toBeInTheDocument();
});

it('renders credential options', () => {
renderGenerator();
expect(screen.getByText('COVID-19 (Pfizer) — Verified')).toBeInTheDocument();
expect(screen.getByText('Influenza 2025 — Pending')).toBeInTheDocument();
});

it('renders proof duration selector', () => {
renderGenerator();
expect(screen.getByLabelText('Proof Duration')).toBeInTheDocument();
expect(screen.getByText('1 hour')).toBeInTheDocument();
expect(screen.getByText('1 day')).toBeInTheDocument();
expect(screen.getByText('1 week')).toBeInTheDocument();
expect(screen.getByText('1 month')).toBeInTheDocument();
expect(screen.getByText('1 year')).toBeInTheDocument();
});

it('renders verification level selector', () => {
renderGenerator();
expect(screen.getByLabelText('Verification Level')).toBeInTheDocument();
expect(screen.getByText('Basic — Status only')).toBeInTheDocument();
expect(screen.getByText('Advanced — Status + issuer')).toBeInTheDocument();
expect(screen.getByText('Full — All credential data')).toBeInTheDocument();
});

it('renders toggle options', () => {
renderGenerator();
expect(screen.getByText('Include issuer information')).toBeInTheDocument();
expect(screen.getByText('Include vaccination date')).toBeInTheDocument();
});

it('renders generate button', () => {
renderGenerator();
const buttons = screen.getAllByText('Generate Proof');
expect(buttons.length).toBeGreaterThanOrEqual(1);
});

it('shows no credentials message when empty', () => {
renderGenerator([]);
expect(screen.getByText('No credentials available. Upload a vaccination record first.')).toBeInTheDocument();
});

it('renders generate button disabled when no credential selected', () => {
renderGenerator();
// Default state has no credential selected, so button should be disabled
const buttons = screen.getAllByText('Generate Proof');
const button = buttons.find((el) => el.tagName === 'BUTTON');
expect(button).toBeDisabled();
});

it('renders duration options in correct order', () => {
renderGenerator();
const select = screen.getByLabelText('Proof Duration') as HTMLSelectElement;
expect(select.value).toBe('86400'); // 1 day default
});

it('renders with empty credentials array', () => {
renderGenerator([]);
const select = screen.getByLabelText('Select Vaccination Credential') as HTMLSelectElement;
expect(select).toBeInTheDocument();
expect(select.options.length).toBe(1); // Only the placeholder
});

it('renders accessible region', () => {
renderGenerator();
const region = screen.getByRole('region', { name: 'Vaccination Proof Generator' });
expect(region).toBeInTheDocument();
});
});
44 changes: 44 additions & 0 deletions frontend/src/components/loading/loading-spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import { motion } from 'framer-motion';

interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
label?: string;
className?: string;
}

const SIZE_CLASSES = {
sm: 'w-4 h-4 border-2',
md: 'w-8 h-8 border-[3px]',
lg: 'w-12 h-12 border-4',
} as const;

export function LoadingSpinner({ size = 'md', label, className = '' }: LoadingSpinnerProps) {
const spinner = (
<motion.div
className={`${SIZE_CLASSES[size]} rounded-full border-green-500/20 border-t-green-400 border-r-green-400 inline-block ${className}`}
aria-label={label || 'Loading'}
aria-live="polite"
animate={{ rotate: 360 }}
transition={{ duration: 0.8, repeat: Infinity, ease: 'linear' }}
>
<span className="sr-only">{label || 'Loading...'}</span>
</motion.div>
);

if (!label) return (
<div role="status" aria-label="Loading" aria-live="polite">
{spinner}
</div>
);

return (
<div className="flex items-center gap-3" role="status" aria-live="polite">
{spinner}
<span className="text-sm text-green-200">{label}</span>
</div>
);
}

export { LoadingSpinner as default };
Loading