Skip to content
Merged
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
629 changes: 629 additions & 0 deletions backend/tests/stellar/sep10-auth.test.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions frontend/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
["@babel/preset-react", { "runtime": "automatic" }]
]
}
16 changes: 16 additions & 0 deletions frontend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
collectCoverageFrom: [
'src/components/**/*.{js,jsx}',
'src/pages/**/*.{js,jsx}'
],
coverageThreshold: {
global: {
branches: 30,
functions: 30,
lines: 40,
statements: 40
}
}
};
8 changes: 8 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"@stellar/freighter-api": "^2.0.0",
"@stellar/stellar-sdk": "^12.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.0"
"react-router-dom": "^6.22.0",
"@stellar/freighter-api": "^2.0.0",
"@stellar/stellar-sdk": "^12.0.0",
"i18next": "^23.11.5",
"react-i18next": "^14.1.2"
},
"devDependencies": {
"@babel/preset-env": "^7.29.2",
"@babel/preset-react": "^7.28.5",
"@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^14.2.0",
"@testing-library/user-event": "^14.5.0",
"@vitejs/plugin-react": "^4.2.1",
"@playwright/test": "^1.40.0",
"vite": "^5.1.4"
Expand Down
82 changes: 82 additions & 0 deletions frontend/src/components/ConfirmMintDialog.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { render, screen, fireEvent } from '@testing-library/react';
import ConfirmMintDialog from './ConfirmMintDialog';

describe('ConfirmMintDialog', () => {
const mockRecord = {
patient_address: 'G12345678901234567890123456789012345678901234567890123456',
vaccine_name: 'COVID-19',
date_administered: '2024-01-15'
};
const mockOnConfirm = jest.fn();
const mockOnCancel = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('renders dialog with record details', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);

expect(screen.getByText(/Confirm Vaccination Mint/i)).toBeInTheDocument();
expect(screen.getByText(/Patient/i)).toBeInTheDocument();
expect(screen.getByText(/Vaccine/i)).toBeInTheDocument();
expect(screen.getByText(/Date/i)).toBeInTheDocument();
});

it('displays patient address', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
expect(screen.getByText(/G12345678/i)).toBeInTheDocument();
});

it('displays vaccine name', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
expect(screen.getByText(/COVID-19/i)).toBeInTheDocument();
});

it('displays date administered', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
expect(screen.getByText(/2024-01-15/i)).toBeInTheDocument();
});

it('shows confirm button', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
expect(screen.getByRole('button', { name: /Confirm & Mint/i })).toBeInTheDocument();
});

it('shows cancel button', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument();
});

it('calls onConfirm when confirm button is clicked', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
fireEvent.click(screen.getByRole('button', { name: /Confirm & Mint/i }));
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
});

it('calls onCancel when cancel button is clicked', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
fireEvent.click(screen.getByRole('button', { name: /Cancel/i }));
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});

it('has correct role and aria attributes', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
const overlay = screen.getByRole('dialog');
expect(overlay).toHaveAttribute('aria-modal', 'true');
});

it('confirm button has autoFocus', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
const confirmButton = screen.getByRole('button', { name: /Confirm & Mint/i });
expect(confirmButton).toHaveFocus();
});

it('prevents Enter key from triggering default behavior', () => {
render(<ConfirmMintDialog record={mockRecord} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />);
const overlay = screen.getByRole('dialog');
const preventDefault = jest.fn();
fireEvent.keyDown(overlay, { key: 'Enter', preventDefault });
expect(preventDefault).not.toHaveBeenCalled(); // Enter is allowed to propagate
});
});
39 changes: 39 additions & 0 deletions frontend/src/components/DarkModeToggle.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render, screen, fireEvent } from '@testing-library/react';
import DarkModeToggle from './DarkModeToggle';

describe('DarkModeToggle', () => {
it('renders toggle button', () => {
render(<DarkModeToggle dark={false} onToggle={() => {}} />);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
});

it('shows moon emoji when in light mode', () => {
render(<DarkModeToggle dark={false} onToggle={() => {}} />);
expect(screen.getByRole('button')).toHaveTextContent('🌙');
});

it('shows sun emoji when in dark mode', () => {
render(<DarkModeToggle dark={true} onToggle={() => {}} />);
expect(screen.getByRole('button')).toHaveTextContent('☀️');
});

it('calls onToggle when clicked', () => {
const handleToggle = jest.fn();
render(<DarkModeToggle dark={false} onToggle={handleToggle} />);
fireEvent.click(screen.getByRole('button'));
expect(handleToggle).toHaveBeenCalledTimes(1);
});

it('has correct aria-label for light mode', () => {
render(<DarkModeToggle dark={false} onToggle={() => {}} />);
// In light mode (dark=false), button says "Switch to dark mode"
expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Switch to dark mode');
});

it('has correct aria-label for dark mode', () => {
render(<DarkModeToggle dark={true} onToggle={() => {}} />);
// In dark mode (dark=true), button says "Switch to light mode"
expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Switch to light mode');
});
});
61 changes: 61 additions & 0 deletions frontend/src/components/FreighterBanner.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render, screen, fireEvent } from '@testing-library/react';
import FreighterBanner from './FreighterBanner';

// Mock useAuth hook
jest.mock('../hooks/useFreighter', () => ({
useAuth: jest.fn(),
}));

import { useAuth } from '../hooks/useFreighter';

describe('FreighterBanner', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders nothing when freighter is installed', () => {
useAuth.mockReturnValue({ freighterInstalled: true });
const { container } = render(<FreighterBanner />);
expect(container).toBeEmptyDOMElement();
});

it('renders banner when freighter is not installed', () => {
useAuth.mockReturnValue({ freighterInstalled: false });
render(<FreighterBanner />);
expect(screen.getByText(/Freighter wallet not detected/i)).toBeInTheDocument();
});

it('renders install link', () => {
useAuth.mockReturnValue({ freighterInstalled: false });
render(<FreighterBanner />);
const link = screen.getByRole('link', { name: /Install Freighter/i });
expect(link).toHaveAttribute('href', 'https://www.freighter.app/');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noreferrer');
});

it('renders dismiss button', () => {
useAuth.mockReturnValue({ freighterInstalled: false });
render(<FreighterBanner />);
expect(screen.getByRole('button', { name: /Dismiss/i })).toBeInTheDocument();
});

it('hides banner after dismiss', () => {
useAuth.mockReturnValue({ freighterInstalled: false });
const { rerender } = render(<FreighterBanner />);
expect(screen.getByText(/Freighter wallet not detected/i)).toBeInTheDocument();

// Dismiss the banner - component uses internal state
fireEvent.click(screen.getByRole('button', { name: /Dismiss/i }));

// After dismiss, the component's internal state changes and banner is hidden
expect(screen.queryByText(/Freighter wallet not detected/i)).not.toBeInTheDocument();
});

it('has correct styling', () => {
useAuth.mockReturnValue({ freighterInstalled: false });
render(<FreighterBanner />);
const banner = screen.getByText(/Freighter wallet not detected/i).closest('div');
expect(banner).toHaveStyle({ background: '#7c3aed', color: '#fff' });
});
});
1 change: 1 addition & 0 deletions frontend/src/components/NFTCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default function NFTCard({ record, onClick }) {

return (
<div
data-testid="nft-card"
role="button"
tabIndex={0}
onClick={onClick}
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/components/NFTCard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { render, screen, fireEvent } from '@testing-library/react';
import NFTCard from './NFTCard';

describe('NFTCard', () => {
const mockRecord = {
token_id: '12345',
vaccine_name: 'COVID-19',
date_administered: '2024-01-15',
issuer: 'GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901234'
};

const mockOnClick = jest.fn();

beforeEach(() => {
mockOnClick.mockClear();
});

it('should render NFT details correctly', () => {
render(<NFTCard record={mockRecord} onClick={mockOnClick} />);

expect(screen.getByText('💉 COVID-19')).toBeInTheDocument();
expect(screen.getByText('#12345')).toBeInTheDocument();
expect(screen.getByText('Date: 2024-01-15')).toBeInTheDocument();
expect(screen.getByText(/Issuer: GABC1234/)).toBeInTheDocument();
});

it('should handle click events', () => {
render(<NFTCard record={mockRecord} onClick={mockOnClick} />);

const card = screen.getByRole('button');
fireEvent.click(card);

expect(mockOnClick).toHaveBeenCalledTimes(1);
});

it('should handle keyboard events (Enter key)', () => {
render(<NFTCard record={mockRecord} onClick={mockOnClick} />);

const card = screen.getByRole('button');
fireEvent.keyDown(card, { key: 'Enter' });

expect(mockOnClick).toHaveBeenCalledTimes(1);
});

it('should handle keyboard events (Space key)', () => {
render(<NFTCard record={mockRecord} onClick={mockOnClick} />);

const card = screen.getByRole('button');
fireEvent.keyDown(card, { key: ' ' });

expect(mockOnClick).toHaveBeenCalledTimes(1);
});

it('should render without onClick handler', () => {
render(<NFTCard record={mockRecord} />);

const card = screen.getByRole('button');
expect(card).toBeInTheDocument();
});

it('should display truncated issuer address', () => {
render(<NFTCard record={mockRecord} />);

expect(screen.getByText(/Issuer: GABC1234/)).toBeInTheDocument();
expect(screen.getByText(/…1234$/)).toBeInTheDocument();
});
});
40 changes: 40 additions & 0 deletions frontend/src/components/NFTCardSkeleton.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/react';
import NFTCardSkeleton from './NFTCardSkeleton';

describe('NFTCardSkeleton', () => {
it('renders skeleton cards with default count of 3', () => {
render(<NFTCardSkeleton />);
const cards = screen.getAllByText((content, element) => {
return element.tagName.toLowerCase() === 'div' &&
element.getAttribute('style')?.includes('border: 1px solid #334155');
});
expect(cards).toHaveLength(3);
});

it('renders skeleton cards with custom count', () => {
render(<NFTCardSkeleton count={5} />);
const cards = screen.getAllByText((content, element) => {
return element.tagName.toLowerCase() === 'div' &&
element.getAttribute('style')?.includes('border: 1px solid #334155');
});
expect(cards).toHaveLength(5);
});

it('renders skeleton cards with count of 1', () => {
render(<NFTCardSkeleton count={1} />);
const cards = screen.getAllByText((content, element) => {
return element.tagName.toLowerCase() === 'div' &&
element.getAttribute('style')?.includes('border: 1px solid #334155');
});
expect(cards).toHaveLength(1);
});

it('renders skeleton with zero count', () => {
render(<NFTCardSkeleton count={0} />);
const cards = screen.queryAllByText((content, element) => {
return element.tagName.toLowerCase() === 'div' &&
element.getAttribute('style')?.includes('border: 1px solid #334155');
});
expect(cards).toHaveLength(0);
});
});
Loading
Loading