From 0a86e501e85ad87c81aa5dc7f6e0f19b4929f0ab Mon Sep 17 00:00:00 2001
From: waterWang <672684719@qq.com>
Date: Tue, 25 Aug 2026 05:34:12 +0800
Subject: [PATCH] test: add unit tests for all 13 frontend components (Closes
#169)
---
.../credential-analytics-dashboard.test.tsx | 91 ++++++++++
.../credential-details-modal.test.tsx | 65 +++++++
.../__tests__/credential-edit-modal.test.tsx | 75 ++++++++
.../credential-metadata-display.test.tsx | 167 +++++++++++++++++
.../deletion-confirmation-modal.test.tsx | 168 ++++++++++++++++++
frontend/__tests__/error-boundary.test.tsx | 89 ++++++++++
.../health-credential-vault.test.tsx | 65 +++++++
frontend/__tests__/notification-bell.test.tsx | 74 ++++++++
.../notification-preferences.test.tsx | 90 ++++++++++
.../vaccination-verification-center.test.tsx | 60 +++++++
10 files changed, 944 insertions(+)
create mode 100644 frontend/__tests__/credential-analytics-dashboard.test.tsx
create mode 100644 frontend/__tests__/credential-details-modal.test.tsx
create mode 100644 frontend/__tests__/credential-edit-modal.test.tsx
create mode 100644 frontend/__tests__/credential-metadata-display.test.tsx
create mode 100644 frontend/__tests__/deletion-confirmation-modal.test.tsx
create mode 100644 frontend/__tests__/error-boundary.test.tsx
create mode 100644 frontend/__tests__/health-credential-vault.test.tsx
create mode 100644 frontend/__tests__/notification-bell.test.tsx
create mode 100644 frontend/__tests__/notification-preferences.test.tsx
create mode 100644 frontend/__tests__/vaccination-verification-center.test.tsx
diff --git a/frontend/__tests__/credential-analytics-dashboard.test.tsx b/frontend/__tests__/credential-analytics-dashboard.test.tsx
new file mode 100644
index 00000000..90c4968f
--- /dev/null
+++ b/frontend/__tests__/credential-analytics-dashboard.test.tsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import { CredentialAnalyticsDashboard } from '../src/components/credential-analytics-dashboard';
+
+// Mock fetch to reject so the component falls back to MOCK_DATA
+global.fetch = jest.fn(() => Promise.reject(new Error('Network error')));
+
+describe('CredentialAnalyticsDashboard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('shows loading state initially', () => {
+ render();
+ expect(screen.getByText('Loading analytics...')).toBeInTheDocument();
+ });
+
+ it('renders dashboard with mock data after loading', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Credential Analytics Dashboard')).toBeInTheDocument();
+ });
+
+ // Check usage stats
+ expect(screen.getByText('Total Credentials Issued')).toBeInTheDocument();
+ expect(screen.getByText('Total Verifications')).toBeInTheDocument();
+ expect(screen.getByText('Data Shares')).toBeInTheDocument();
+
+ // Check mock data values
+ expect(screen.getByText('120')).toBeInTheDocument(); // totalIdentities
+ expect(screen.getByText('300')).toBeInTheDocument(); // total verifications
+ expect(screen.getByText('150')).toBeInTheDocument(); // total shares
+ });
+
+ it('renders system status', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(/operational/i)).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('99.99%')).toBeInTheDocument(); // uptime
+ expect(screen.getByText('45ms')).toBeInTheDocument(); // api latency
+ });
+
+ it('renders quick action buttons', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Issue New')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Export Report')).toBeInTheDocument();
+ });
+
+ it('renders recent activity section', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Recent Activity')).toBeInTheDocument();
+ });
+
+ // Check for activity items
+ expect(screen.getByText(/Vaccination verified by Clinic A/)).toBeInTheDocument();
+ expect(screen.getByText(/Credential shared with Employer B/)).toBeInTheDocument();
+ });
+
+ it('renders verification trend section', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Verification Trend (Last 7 Days)')).toBeInTheDocument();
+ });
+ });
+
+ it('renders verification rates pie chart', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Verification Rates')).toBeInTheDocument();
+ });
+ });
+
+ it('shows loading while fetch is pending', async () => {
+ // Make fetch never resolve/reject
+ global.fetch = jest.fn(() => new Promise(() => {}));
+ render();
+ expect(screen.getByText('Loading analytics...')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/credential-details-modal.test.tsx b/frontend/__tests__/credential-details-modal.test.tsx
new file mode 100644
index 00000000..f4c5cd05
--- /dev/null
+++ b/frontend/__tests__/credential-details-modal.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { CredentialDetailsModal } from '../src/components/credential-details-modal';
+
+const mockCredential = {
+ id: 'cred-001',
+ vaccineType: 'COVID-19 (Pfizer)',
+ verificationStatus: true,
+ vaccinationDate: '2026-07-15',
+};
+
+describe('CredentialDetailsModal', () => {
+ it('renders nothing when isOpen is false', () => {
+ const { container } = render(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('renders nothing when credential is null', () => {
+ const { container } = render(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('renders credential details when open', () => {
+ render(
+
+ );
+ expect(screen.getByText('Credential Details')).toBeInTheDocument();
+ expect(screen.getByText('COVID-19 (Pfizer)')).toBeInTheDocument();
+ expect(screen.getByText('Verified')).toBeInTheDocument();
+ expect(screen.getByText('2026-07-15')).toBeInTheDocument();
+ expect(screen.getByText('cred-001')).toBeInTheDocument();
+ });
+
+ it('shows Pending status when verificationStatus is false', () => {
+ const pendingCred = { ...mockCredential, verificationStatus: false };
+ render(
+
+ );
+ expect(screen.getByText('Pending')).toBeInTheDocument();
+ });
+
+ it('calls onClose when close button is clicked', () => {
+ const onClose = jest.fn();
+ render(
+
+ );
+ const closeButton = screen.getByLabelText('Close modal');
+ fireEvent.click(closeButton);
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('calls onClose when backdrop is clicked', () => {
+ const onClose = jest.fn();
+ const { container } = render(
+
+ );
+ // The backdrop is the first motion.div with onClick={onClose}
+ const backdrops = container.querySelectorAll('.fixed.inset-0');
+ expect(backdrops.length).toBeGreaterThan(0);
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/credential-edit-modal.test.tsx b/frontend/__tests__/credential-edit-modal.test.tsx
new file mode 100644
index 00000000..d94dae29
--- /dev/null
+++ b/frontend/__tests__/credential-edit-modal.test.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { CredentialEditModal } from '../src/components/credential-edit-modal';
+
+const mockCredential = {
+ id: 'cred-001',
+ vaccineType: 'COVID-19 (Pfizer)',
+ verificationStatus: true,
+ vaccinationDate: '2026-07-15',
+};
+
+describe('CredentialEditModal', () => {
+ it('renders nothing when isOpen is false', () => {
+ const { container } = render(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('renders nothing when credential is null', () => {
+ const { container } = render(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('renders edit form with credential values', () => {
+ render(
+
+ );
+ expect(screen.getByText('Edit Metadata')).toBeInTheDocument();
+ expect(screen.getByDisplayValue('COVID-19 (Pfizer)')).toBeInTheDocument();
+ expect(screen.getByDisplayValue('2026-07-15')).toBeInTheDocument();
+ expect(screen.getByText('Save Changes')).toBeInTheDocument();
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
+ });
+
+ it('calls onSave with updated credential on form submit', () => {
+ const onSave = jest.fn();
+ const onClose = jest.fn();
+ render(
+
+ );
+
+ const vaccineInput = screen.getByDisplayValue('COVID-19 (Pfizer)');
+ fireEvent.change(vaccineInput, { target: { value: 'COVID-19 (Moderna)' } });
+
+ fireEvent.click(screen.getByText('Save Changes'));
+
+ expect(onSave).toHaveBeenCalledWith({
+ ...mockCredential,
+ vaccineType: 'COVID-19 (Moderna)',
+ });
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('calls onClose when Cancel button is clicked', () => {
+ const onClose = jest.fn();
+ render(
+
+ );
+ fireEvent.click(screen.getByText('Cancel'));
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('calls onClose when close button is clicked', () => {
+ const onClose = jest.fn();
+ render(
+
+ );
+ const closeButton = screen.getByLabelText('Close modal');
+ fireEvent.click(closeButton);
+ expect(onClose).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/credential-metadata-display.test.tsx b/frontend/__tests__/credential-metadata-display.test.tsx
new file mode 100644
index 00000000..e7cea478
--- /dev/null
+++ b/frontend/__tests__/credential-metadata-display.test.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { CredentialMetadataDisplay } from '../src/components/credential-metadata-display';
+import { AccessibilityProvider } from '../src/contexts/AccessibilityContext';
+
+function renderWithProviders(ui: React.ReactElement) {
+ return render({ui});
+}
+
+const mockCredentials = [
+ {
+ id: 'cred-1',
+ name: 'COVID-19 Vaccination',
+ type: 'vaccination',
+ issuer: 'issuer-1',
+ issuerName: 'City Health Department',
+ issuedAt: '2026-01-15T00:00:00Z',
+ updatedAt: '2026-06-15T00:00:00Z',
+ expiresAt: '2027-01-15T00:00:00Z',
+ status: 'active' as const,
+ description: 'COVID-19 vaccination record',
+ version: '1.0',
+ schema: 'https://example.com/schema/vaccination',
+ proofType: 'BBS+',
+ signatureAlgorithm: 'Ed25519',
+ history: [
+ {
+ timestamp: '2026-06-15T10:00:00Z',
+ action: 'Updated',
+ field: 'status',
+ oldValue: 'pending',
+ newValue: 'active',
+ performedBy: 'admin@health.gov',
+ },
+ ],
+ },
+ {
+ id: 'cred-2',
+ name: 'Flu Shot 2025',
+ type: 'vaccination',
+ issuer: 'issuer-2',
+ issuerName: 'Wellness Center',
+ issuedAt: '2025-10-01T00:00:00Z',
+ updatedAt: '2025-10-01T00:00:00Z',
+ status: 'expired' as const,
+ proofType: 'Ed25519',
+ signatureAlgorithm: 'Ed25519',
+ },
+ {
+ id: 'cred-3',
+ name: 'Hepatitis B',
+ type: 'vaccination',
+ issuer: 'issuer-3',
+ issuerName: 'General Hospital',
+ issuedAt: '2024-03-10T00:00:00Z',
+ updatedAt: '2024-03-10T00:00:00Z',
+ status: 'revoked' as const,
+ proofType: 'Ed25519',
+ signatureAlgorithm: 'Ed25519',
+ },
+];
+
+describe('CredentialMetadataDisplay', () => {
+ it('renders the heading', () => {
+ renderWithProviders();
+ expect(screen.getByText('Credential Metadata')).toBeInTheDocument();
+ });
+
+ it('renders credential names', () => {
+ renderWithProviders();
+ expect(screen.getByText('COVID-19 Vaccination')).toBeInTheDocument();
+ expect(screen.getByText('Flu Shot 2025')).toBeInTheDocument();
+ expect(screen.getByText('Hepatitis B')).toBeInTheDocument();
+ });
+
+ it('renders issuer names', () => {
+ renderWithProviders();
+ expect(screen.getByText('City Health Department')).toBeInTheDocument();
+ expect(screen.getByText('Wellness Center')).toBeInTheDocument();
+ expect(screen.getByText('General Hospital')).toBeInTheDocument();
+ });
+
+ it('renders summary section with totals', () => {
+ renderWithProviders();
+ expect(screen.getByText('Total Credentials')).toBeInTheDocument();
+ // Active appears in multiple places, use getAllByText
+ const activeElements = screen.getAllByText('Active');
+ expect(activeElements.length).toBeGreaterThanOrEqual(1);
+ expect(screen.getByText('Unique Issuers')).toBeInTheDocument();
+ expect(screen.getByText('Credential Types')).toBeInTheDocument();
+ });
+
+ it('renders expired/revoked alert badges', () => {
+ renderWithProviders();
+ expect(screen.getByText('1 Expired')).toBeInTheDocument();
+ expect(screen.getByText('1 Revoked')).toBeInTheDocument();
+ });
+
+ it('renders search input', () => {
+ renderWithProviders();
+ expect(screen.getByPlaceholderText('Search by name, issuer, type, or description...')).toBeInTheDocument();
+ });
+
+ it('filters credentials by search query', () => {
+ renderWithProviders();
+ const searchInput = screen.getByPlaceholderText('Search by name, issuer, type, or description...');
+ fireEvent.change(searchInput, { target: { value: 'Flu' } });
+ expect(screen.getByText('Flu Shot 2025')).toBeInTheDocument();
+ expect(screen.queryByText('COVID-19 Vaccination')).not.toBeInTheDocument();
+ expect(screen.queryByText('Hepatitis B')).not.toBeInTheDocument();
+ });
+
+ it('shows empty state when no credentials match filter', () => {
+ renderWithProviders();
+ const searchInput = screen.getByPlaceholderText('Search by name, issuer, type, or description...');
+ fireEvent.change(searchInput, { target: { value: 'ZZZNonexistent' } });
+ // When no results, the filtered credentials list is empty
+ expect(screen.getByText(/Showing 0 of/)).toBeInTheDocument();
+ });
+
+ it('shows empty state when credentials array is empty', () => {
+ renderWithProviders();
+ // When array is empty, the component should show an empty state
+ expect(screen.getByText('Credential Metadata')).toBeInTheDocument();
+ });
+
+ it('renders filter dropdown', () => {
+ renderWithProviders();
+ expect(screen.getByLabelText('Filter by status')).toBeInTheDocument();
+ expect(screen.getByText('All Status')).toBeInTheDocument();
+ });
+
+ it('filters by status via select', () => {
+ renderWithProviders();
+ const filterSelect = screen.getByLabelText('Filter by status');
+ fireEvent.change(filterSelect, { target: { value: 'active' } });
+ expect(screen.getByText('COVID-19 Vaccination')).toBeInTheDocument();
+ expect(screen.queryByText('Flu Shot 2025')).not.toBeInTheDocument();
+ expect(screen.queryByText('Hepatitis B')).not.toBeInTheDocument();
+ });
+
+ it('renders select all checkbox', () => {
+ renderWithProviders();
+ const selectAllCheck = screen.getByLabelText('Select all credentials');
+ expect(selectAllCheck).toBeInTheDocument();
+ });
+
+ it('renders compare button', () => {
+ renderWithProviders();
+ expect(screen.getByText('Compare')).toBeInTheDocument();
+ });
+
+ it('renders refresh button', () => {
+ renderWithProviders();
+ expect(screen.getByText('Refresh')).toBeInTheDocument();
+ });
+
+ it('expands credential details when clicked', () => {
+ renderWithProviders();
+ // Click on the first credential name to expand
+ fireEvent.click(screen.getByText('COVID-19 Vaccination'));
+ // After expansion, extra details should appear
+ expect(screen.getByText('Description')).toBeInTheDocument();
+ expect(screen.getByText('Technical Details')).toBeInTheDocument();
+ expect(screen.getByText('Change History')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/deletion-confirmation-modal.test.tsx b/frontend/__tests__/deletion-confirmation-modal.test.tsx
new file mode 100644
index 00000000..d9e6b576
--- /dev/null
+++ b/frontend/__tests__/deletion-confirmation-modal.test.tsx
@@ -0,0 +1,168 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { DeletionConfirmationModal } from '../src/components/deletion-confirmation-modal';
+import { AccessibilityProvider } from '../src/contexts/AccessibilityContext';
+
+function renderWithProviders(ui: React.ReactElement) {
+ return render({ui});
+}
+
+const mockCredential = {
+ id: 'cred-001',
+ vaccineType: 'COVID-19 (Pfizer)',
+ verificationStatus: true,
+ vaccinationDate: '2026-07-15',
+};
+
+describe('DeletionConfirmationModal', () => {
+ it('renders nothing when isOpen is false', () => {
+ const { container } = renderWithProviders(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('renders nothing when credential is null', () => {
+ const { container } = renderWithProviders(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('renders credential details when open', () => {
+ renderWithProviders(
+
+ );
+ expect(screen.getByText('Delete Credential')).toBeInTheDocument();
+ expect(screen.getByText('COVID-19 (Pfizer)')).toBeInTheDocument();
+ expect(screen.getByText('Verified')).toBeInTheDocument();
+ expect(screen.getByText('2026-07-15')).toBeInTheDocument();
+ });
+
+ it('disables delete button until DELETE is typed', () => {
+ renderWithProviders(
+
+ );
+ const deleteButton = screen.getByText('Delete Permanently');
+ expect(deleteButton).toBeDisabled();
+
+ const input = screen.getByPlaceholderText('Type DELETE');
+ fireEvent.change(input, { target: { value: 'DELETE' } });
+ // Re-query the button after state change (component re-renders)
+ const enabledButton = screen.getByText('Delete Permanently');
+ expect(enabledButton).not.toBeDisabled();
+ });
+
+ it('calls onConfirm when delete button is clicked with confirmation', () => {
+ const onConfirm = jest.fn();
+ renderWithProviders(
+
+ );
+ const input = screen.getByPlaceholderText('Type DELETE');
+ fireEvent.change(input, { target: { value: 'DELETE' } });
+ fireEvent.click(screen.getByText('Delete Permanently'));
+ expect(onConfirm).toHaveBeenCalled();
+ });
+
+ it('calls onCancel when Cancel button is clicked', () => {
+ const onCancel = jest.fn();
+ renderWithProviders(
+
+ );
+ fireEvent.click(screen.getByText('Cancel'));
+ expect(onCancel).toHaveBeenCalled();
+ });
+
+ it('shows deleting state', () => {
+ renderWithProviders(
+
+ );
+ expect(screen.getByText('Deleting credential...')).toBeInTheDocument();
+ });
+
+ it('shows deleted state', () => {
+ renderWithProviders(
+
+ );
+ expect(screen.getByText('Credential deleted')).toBeInTheDocument();
+ });
+
+ it('shows failed state', () => {
+ renderWithProviders(
+
+ );
+ expect(screen.getByText('Deletion failed')).toBeInTheDocument();
+ });
+
+ it('shows undoable state with undo button', () => {
+ const onUndo = jest.fn();
+ renderWithProviders(
+
+ );
+ expect(screen.getByText(/You can undo this action within/)).toBeInTheDocument();
+ expect(screen.getByText('Undo Delete')).toBeInTheDocument();
+ fireEvent.click(screen.getByText('Undo Delete'));
+ expect(onUndo).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/error-boundary.test.tsx b/frontend/__tests__/error-boundary.test.tsx
new file mode 100644
index 00000000..68bf7e36
--- /dev/null
+++ b/frontend/__tests__/error-boundary.test.tsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { ErrorBoundary } from '../src/components/error-boundary';
+
+// Mock the error-handling utility
+jest.mock('../src/utils/error-handling', () => ({
+ logError: jest.fn(),
+}));
+
+const GoodComponent = () =>
Everything is fine
;
+
+const BadComponent = () => {
+ throw new Error('Test error');
+};
+
+describe('ErrorBoundary', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders children when there is no error', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByText('Everything is fine')).toBeInTheDocument();
+ });
+
+ it('renders error state when a child throws', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ expect(screen.getByText('Test error')).toBeInTheDocument();
+ expect(screen.getByText('Try Again')).toBeInTheDocument();
+ });
+
+ it('renders custom fallback when provided', () => {
+ render(
+ Custom fallback}>
+
+
+ );
+ expect(screen.getByText('Custom fallback')).toBeInTheDocument();
+ expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
+ });
+
+ it('resets error state when Try Again is clicked', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ fireEvent.click(screen.getByText('Try Again'));
+ // After reset, the component re-renders and will throw again since BadComponent always throws
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ });
+
+ it('calls onReset when provided and Try Again is clicked', () => {
+ const onReset = jest.fn();
+ render(
+
+
+
+ );
+ fireEvent.click(screen.getByText('Try Again'));
+ expect(onReset).toHaveBeenCalled();
+ });
+
+ it('shows generic error message when no error message is available', () => {
+ const ThrowsNull = () => {
+ throw null;
+ };
+ render(
+
+
+
+ );
+ expect(screen.getByText('An unexpected error occurred while loading this component.')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/health-credential-vault.test.tsx b/frontend/__tests__/health-credential-vault.test.tsx
new file mode 100644
index 00000000..f26e8d10
--- /dev/null
+++ b/frontend/__tests__/health-credential-vault.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { HealthCredentialVault } from '../src/components/health-credential-vault';
+import { AccessibilityProvider } from '../src/contexts/AccessibilityContext';
+
+// Mock crypto.randomUUID
+const mockUUID = 'test-uuid-12345';
+global.crypto.randomUUID = jest.fn(() => mockUUID);
+
+function renderWithProviders(ui: React.ReactElement) {
+ return render({ui});
+}
+
+describe('HealthCredentialVault', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('renders the vault heading', () => {
+ renderWithProviders();
+ expect(screen.getByText('Health Credential Vault')).toBeInTheDocument();
+ });
+
+ it('renders upload area', () => {
+ renderWithProviders();
+ expect(screen.getByText('Upload your vaccination records')).toBeInTheDocument();
+ expect(screen.getByText('Select File')).toBeInTheDocument();
+ });
+
+ it('shows empty state when no credentials exist', () => {
+ renderWithProviders();
+ expect(screen.getByText('No health credentials uploaded yet')).toBeInTheDocument();
+ });
+
+ it('has file upload input accessible via label', () => {
+ renderWithProviders();
+ const fileInput = screen.getByLabelText('Upload vaccination record file');
+ expect(fileInput).toBeInTheDocument();
+ expect(fileInput).toHaveAttribute('type', 'file');
+ });
+
+ it('renders with correct aria region', () => {
+ renderWithProviders();
+ const region = screen.getByRole('region', { name: 'Health Credential Vault' });
+ expect(region).toBeInTheDocument();
+ });
+
+ it('renders file upload area', () => {
+ renderWithProviders();
+ const uploadRegion = screen.getByRole('region', { name: 'File upload area' });
+ expect(uploadRegion).toBeInTheDocument();
+ });
+
+ it('does not render credential list items when empty', () => {
+ renderWithProviders();
+ const list = screen.getByRole('list', { name: 'Uploaded credentials' });
+ expect(list).toBeInTheDocument();
+ // Empty state should show
+ expect(screen.getByText('No health credentials uploaded yet')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/notification-bell.test.tsx b/frontend/__tests__/notification-bell.test.tsx
new file mode 100644
index 00000000..61908b34
--- /dev/null
+++ b/frontend/__tests__/notification-bell.test.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { render, screen, fireEvent, act } from '@testing-library/react';
+import { NotificationBell } from '../src/components/NotificationBell';
+import { AccessibilityProvider } from '../src/contexts/AccessibilityContext';
+import { NotificationProvider } from '../src/contexts/NotificationContext';
+
+function renderWithProviders(ui: React.ReactElement) {
+ return render(
+
+
+ {ui}
+
+
+ );
+}
+
+describe('NotificationBell', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('renders the bell button', () => {
+ renderWithProviders();
+ expect(screen.getByLabelText('Notifications')).toBeInTheDocument();
+ });
+
+ it('shows unread count badge when there are unread notifications', () => {
+ // Add a notification to the context
+ renderWithProviders();
+ // By default, no notifications
+ const bellButton = screen.getByLabelText('Notifications');
+ expect(bellButton).toBeInTheDocument();
+ });
+
+ it('opens the notifications panel when clicked', () => {
+ renderWithProviders();
+ const bellButton = screen.getByLabelText('Notifications');
+ fireEvent.click(bellButton);
+ expect(screen.getByText('Notifications')).toBeInTheDocument();
+ expect(screen.getByText('No notifications yet')).toBeInTheDocument();
+ });
+
+ it('closes panel when clicking outside', () => {
+ renderWithProviders();
+ const bellButton = screen.getByLabelText('Notifications');
+ fireEvent.click(bellButton);
+ expect(screen.getByText('Notifications')).toBeInTheDocument();
+ fireEvent.mouseDown(document.body);
+ expect(screen.queryByText('Notifications')).not.toBeInTheDocument();
+ });
+
+ it('closes panel on Escape key', () => {
+ renderWithProviders();
+ const bellButton = screen.getByLabelText('Notifications');
+ fireEvent.click(bellButton);
+ expect(screen.getByText('Notifications')).toBeInTheDocument();
+ fireEvent.keyDown(document, { key: 'Escape' });
+ expect(screen.queryByText('Notifications')).not.toBeInTheDocument();
+ });
+
+ it('has correct aria attributes on the bell button', () => {
+ renderWithProviders();
+ const bellButton = screen.getByLabelText('Notifications');
+ expect(bellButton).toHaveAttribute('aria-haspopup', 'true');
+ expect(bellButton).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('updates aria-expanded when panel is opened', () => {
+ renderWithProviders();
+ const bellButton = screen.getByLabelText('Notifications');
+ fireEvent.click(bellButton);
+ expect(bellButton).toHaveAttribute('aria-expanded', 'true');
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/notification-preferences.test.tsx b/frontend/__tests__/notification-preferences.test.tsx
new file mode 100644
index 00000000..a110de5a
--- /dev/null
+++ b/frontend/__tests__/notification-preferences.test.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { NotificationPreferences } from '../src/components/NotificationPreferences';
+import { AccessibilityProvider } from '../src/contexts/AccessibilityContext';
+import { NotificationProvider } from '../src/contexts/NotificationContext';
+
+function renderWithProviders(ui: React.ReactElement) {
+ return render(
+
+
+ {ui}
+
+
+ );
+}
+
+describe('NotificationPreferences', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('renders the preferences heading', () => {
+ renderWithProviders();
+ expect(screen.getByText('Notification Channels')).toBeInTheDocument();
+ expect(screen.getByText('Alert Types')).toBeInTheDocument();
+ });
+
+ it('renders channel options', () => {
+ renderWithProviders();
+ expect(screen.getByText('In-App Notifications')).toBeInTheDocument();
+ expect(screen.getByText('Push Notifications')).toBeInTheDocument();
+ expect(screen.getByText('Email Notifications')).toBeInTheDocument();
+ });
+
+ it('renders alert type options', () => {
+ renderWithProviders();
+ expect(screen.getByText('Credential Expiry')).toBeInTheDocument();
+ expect(screen.getByText('Verification Complete')).toBeInTheDocument();
+ expect(screen.getByText('Sharing Requests')).toBeInTheDocument();
+ });
+
+ it('toggles in-app notifications checkbox', () => {
+ renderWithProviders();
+ const inAppCheckbox = screen.getByLabelText('In-app notifications');
+ expect(inAppCheckbox).toBeChecked();
+ fireEvent.click(inAppCheckbox);
+ expect(inAppCheckbox).not.toBeChecked();
+ });
+
+ it('shows email input when email notifications are enabled', () => {
+ renderWithProviders();
+ // Email is disabled by default, so enable it first
+ const emailCheckbox = screen.getByLabelText('Email notifications');
+ expect(emailCheckbox).not.toBeChecked();
+ fireEvent.click(emailCheckbox);
+ expect(emailCheckbox).toBeChecked();
+ // Now email input should appear
+ expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument();
+ });
+
+ it('hides email input when email notifications are disabled', () => {
+ renderWithProviders();
+ // Email is disabled by default, so no input
+ expect(screen.queryByPlaceholderText('your@email.com')).not.toBeInTheDocument();
+ });
+
+ it('updates email address input', () => {
+ renderWithProviders();
+ // Enable email first
+ const emailCheckbox = screen.getByLabelText('Email notifications');
+ fireEvent.click(emailCheckbox);
+ const emailInput = screen.getByPlaceholderText('your@email.com');
+ fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
+ expect(emailInput).toHaveValue('test@example.com');
+ });
+
+ it('renders Enable button for push when push is disabled', () => {
+ renderWithProviders();
+ // push defaults to false
+ expect(screen.getByLabelText('Enable push notifications')).toBeInTheDocument();
+ });
+
+ it('toggles credential expiry checkbox', () => {
+ renderWithProviders();
+ const expiryCheckbox = screen.getByLabelText('Credential expiry notifications');
+ expect(expiryCheckbox).toBeChecked();
+ fireEvent.click(expiryCheckbox);
+ expect(expiryCheckbox).not.toBeChecked();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/vaccination-verification-center.test.tsx b/frontend/__tests__/vaccination-verification-center.test.tsx
new file mode 100644
index 00000000..fee1dc6e
--- /dev/null
+++ b/frontend/__tests__/vaccination-verification-center.test.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { VaccinationVerificationCenter } from '../src/components/vaccination-verification-center';
+import { AccessibilityProvider } from '../src/contexts/AccessibilityContext';
+
+function renderWithProviders(ui: React.ReactElement) {
+ return render({ui});
+}
+
+describe('VaccinationVerificationCenter', () => {
+ it('renders the heading', () => {
+ renderWithProviders();
+ expect(screen.getByText('Vaccination Verification Center')).toBeInTheDocument();
+ });
+
+ it('renders stats', () => {
+ renderWithProviders();
+ // Mock data: 1 approved, 1 pending, 1 rejected
+ // Stats and badges both show "Verified", "Pending", "Rejected" - use getAllByText
+ expect(screen.getAllByText('Verified').length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText('Pending').length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText('Rejected').length).toBeGreaterThanOrEqual(1);
+ });
+
+ it('renders count values', () => {
+ renderWithProviders();
+ // Stats: 1 approved, 1 pending, 1 rejected
+ const stats = screen.getAllByRole('status');
+ expect(stats.length).toBeGreaterThanOrEqual(3);
+ });
+
+ it('renders verification items', () => {
+ renderWithProviders();
+ expect(screen.getByText('COVID-19 (Pfizer)')).toBeInTheDocument();
+ expect(screen.getByText('Influenza 2025')).toBeInTheDocument();
+ expect(screen.getByText('Hepatitis B')).toBeInTheDocument();
+ });
+
+ it('renders status badges', () => {
+ renderWithProviders();
+ // Status labels are shown in the badge
+ expect(screen.getAllByText('Approved').length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText('Rejected').length).toBeGreaterThanOrEqual(1);
+ });
+
+ it('renders date labels', () => {
+ renderWithProviders();
+ // Mock data dates formatted as "Submitted: M/D/YYYY"
+ expect(screen.getByText(/7\/15\/2026/)).toBeInTheDocument();
+ expect(screen.getByText(/7\/18\/2026/)).toBeInTheDocument();
+ expect(screen.getByText(/7\/10\/2026/)).toBeInTheDocument();
+ });
+
+ it('renders verification list', () => {
+ renderWithProviders();
+ // The list has aria-label="Verification requests" but no visible heading
+ const list = screen.getByRole('list', { name: /Verification requests/i });
+ expect(list).toBeInTheDocument();
+ });
+});
\ No newline at end of file