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
67 changes: 67 additions & 0 deletions src/components/dashboard/__tests__/RecentTransactions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { RecentTransactions } from '../RecentTransactions';

describe('RecentTransactions', () => {
it('should render the dashboard transaction list heading and subtitle', () => {
render(<RecentTransactions />);

expect(screen.getByText('Recent Activity')).toBeInTheDocument();
expect(
screen.getByText('Latest transactions and income')
).toBeInTheDocument();
});

it('should render all populated transaction rows', () => {
render(<RecentTransactions />);

expect(
screen.getByText('Manhattan Tower Suite')
).toBeInTheDocument();
expect(screen.getByText('Tech Hub Office Complex')).toBeInTheDocument();
expect(screen.getByText('Downtown Luxury Lofts')).toBeInTheDocument();
expect(screen.getByText('Sunset Beach Villa')).toBeInTheDocument();
expect(screen.getByText('Mixed-Use Development')).toBeInTheDocument();
});

it('should show the correct transaction type labels', () => {
render(<RecentTransactions />);

// Two income rows, two purchase rows and one sale row in the static list
expect(screen.getAllByText('Rental Income')).toHaveLength(2);
expect(screen.getAllByText('Purchase')).toHaveLength(2);
expect(screen.getByText('Sale')).toBeInTheDocument();
});

it('should format amounts with a directional sign and thousands separators', () => {
render(<RecentTransactions />);

// Income and purchases are prefixed with "+", sales with "-"
expect(screen.getByText('+$3,280')).toBeInTheDocument();
expect(screen.getByText('+$45,000')).toBeInTheDocument();
expect(screen.getByText('-$12,500')).toBeInTheDocument();
});

it('should render token counts for transactions that include them', () => {
render(<RecentTransactions />);

expect(screen.getByText('90 tokens')).toBeInTheDocument();
expect(screen.getByText('25 tokens')).toBeInTheDocument();
expect(screen.getByText('55 tokens')).toBeInTheDocument();
});

it('should render the completed/pending status for each transaction', () => {
render(<RecentTransactions />);

// Four completed rows and one pending row in the static list
expect(screen.getAllByText('✓ Completed')).toHaveLength(4);
expect(screen.getByText('⏳ Pending')).toBeInTheDocument();
});

it('should render a formatted date for each transaction row', () => {
render(<RecentTransactions />);

// Dates are rendered via toLocaleDateString (e.g. "Jan 20")
expect(screen.getAllByText(/Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec/).length).toBeGreaterThan(0);
});
});
177 changes: 177 additions & 0 deletions src/components/property/__tests__/ImageGallery.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ImageGallery } from '../ImageGallery';
import { ImageLightbox } from '../ImageLightbox';

// Mock next/image to render a plain img so tests don't hit loader/hostname validation.
// Strip next/image-only props (priority, sizes, fill) that are not valid img attributes.
type NextImageMockProps = React.ImgHTMLAttributes<HTMLImageElement> & {
priority?: boolean;
fill?: boolean;
sizes?: string;
};

jest.mock('next/image', () => ({
__esModule: true,
default: ({
priority: _priority,
fill: _fill,
sizes: _sizes,
...imgProps
}: NextImageMockProps) => <img {...imgProps} />,
}));

const images = [
'https://images.unsplash.com/photo-1',
'https://images.unsplash.com/photo-2',
'https://images.unsplash.com/photo-3',
];

describe('ImageGallery', () => {
it('should render the main image with the property name alt text', () => {
render(<ImageGallery images={images} propertyName="Sunset Villa" />);

const mainImage = screen.getByAltText('Sunset Villa');
expect(mainImage).toBeInTheDocument();
});

it('should render thumbnails when there is more than one image', () => {
render(<ImageGallery images={images} propertyName="Sunset Villa" />);

expect(screen.getByAltText('Sunset Villa - Image 2')).toBeInTheDocument();
expect(screen.getByAltText('Sunset Villa - Image 3')).toBeInTheDocument();
});

it('should open the lightbox when the main image is clicked', () => {
render(<ImageGallery images={images} propertyName="Sunset Villa" />);

fireEvent.click(screen.getByAltText('Sunset Villa'));

// Lightbox opens showing image counter "1 / 3"
expect(screen.getByText('1 / 3')).toBeInTheDocument();
expect(screen.getByLabelText('Close lightbox')).toBeInTheDocument();
});

it('should open the lightbox at the correct index when a thumbnail is clicked', () => {
render(<ImageGallery images={images} propertyName="Sunset Villa" />);

fireEvent.click(screen.getByAltText('Sunset Villa - Image 2'));

// Second image is shown as current (counter "2 / 3")
expect(screen.getByText('2 / 3')).toBeInTheDocument();
});

it('should render a placeholder when there are no images', () => {
render(<ImageGallery images={[]} propertyName="Sunset Villa" />);

expect(screen.getByText('No images available')).toBeInTheDocument();
expect(screen.queryByAltText('Sunset Villa')).not.toBeInTheDocument();
});
});

describe('ImageLightbox', () => {
it('should render nothing when closed', () => {
const { container } = render(
<ImageLightbox
images={images}
isOpen={false}
onClose={jest.fn()}
/>
);

expect(container.firstChild).toBeNull();
});

it('should render the current image and counter when open', () => {
render(
<ImageLightbox
images={images}
initialIndex={0}
isOpen
onClose={jest.fn()}
/>
);

expect(screen.getByText('1 / 3')).toBeInTheDocument();
expect(screen.getByAltText('Property image 1')).toBeInTheDocument();
});

it('should navigate to the next image and wrap around', () => {
render(
<ImageLightbox
images={images}
initialIndex={0}
isOpen
onClose={jest.fn()}
/>
);

fireEvent.click(screen.getByLabelText('Next image'));
expect(screen.getByText('2 / 3')).toBeInTheDocument();

fireEvent.click(screen.getByLabelText('Next image'));
expect(screen.getByText('3 / 3')).toBeInTheDocument();

// Wraps back to the first image
fireEvent.click(screen.getByLabelText('Next image'));
expect(screen.getByText('1 / 3')).toBeInTheDocument();
});

it('should navigate to the previous image and wrap around', () => {
render(
<ImageLightbox
images={images}
initialIndex={0}
isOpen
onClose={jest.fn()}
/>
);

fireEvent.click(screen.getByLabelText('Previous image'));
expect(screen.getByText('3 / 3')).toBeInTheDocument();
});

it('should jump to an image when a thumbnail is clicked', () => {
render(
<ImageLightbox
images={images}
initialIndex={0}
isOpen
onClose={jest.fn()}
/>
);

fireEvent.click(screen.getByAltText('Thumbnail 3'));
expect(screen.getByText('3 / 3')).toBeInTheDocument();
});

it('should call onClose when the close button is clicked', () => {
const onClose = jest.fn();
render(
<ImageLightbox
images={images}
initialIndex={0}
isOpen
onClose={onClose}
/>
);

fireEvent.click(screen.getByLabelText('Close lightbox'));
expect(onClose).toHaveBeenCalledTimes(1);
});

it('should call onClose when the Escape key is pressed', () => {
const onClose = jest.fn();
render(
<ImageLightbox
images={images}
initialIndex={0}
isOpen
onClose={onClose}
/>
);

fireEvent.keyDown(document, { key: 'Escape' });
expect(onClose).toHaveBeenCalledTimes(1);
});
});
93 changes: 93 additions & 0 deletions src/components/property/__tests__/PdfViewerModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import PdfViewerModal from '../PdfViewerModal';
import type { PropertyDocument } from '../PdfViewerModal';

// Mock react-pdf with a configurable Document so we can exercise load and error states.
const mockDocument = jest.fn(
(props: {
file?: string;
onLoadSuccess?: () => void;
onError?: () => void;
children?: React.ReactNode;
}) => {
return {
file: props.file,
onLoadSuccess: props.onLoadSuccess,
onError: props.onError,
children: props.children,
};
}
);

// react-pdf is not a declared dependency yet, so mock it as virtual to avoid resolution errors.
jest.mock('react-pdf', () => ({
Document: (props: {
file?: string;
onLoadSuccess?: () => void;
onError?: () => void;
children?: React.ReactNode;
}) => {
mockDocument(props);
return (
<div data-testid="pdf-document" data-fail={props.file === 'https://example.com/broken.pdf' ? 'true' : 'false'}>
{props.children}
</div>
);
},
Page: () => <div data-testid="pdf-page" />,
}), { virtual: true });

const mockDoc: PropertyDocument = {
id: 'doc-1',
name: 'Property Deed',
url: 'https://example.com/deed.pdf',
category: 'Legal',
verified: true,
};

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

it('should render the modal shell with a close button when open', () => {
render(<PdfViewerModal doc={mockDoc} onClose={jest.fn()} />);

expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
});

it('should pass the document url to the PDF Document component', () => {
render(<PdfViewerModal doc={mockDoc} onClose={jest.fn()} />);

expect(mockDocument).toHaveBeenCalledTimes(1);
expect(mockDocument.mock.calls[0][0].file).toBe(mockDoc.url);
});

it('should render the PDF page when a document is provided', () => {
render(<PdfViewerModal doc={mockDoc} onClose={jest.fn()} />);

expect(screen.getByTestId('pdf-page')).toBeInTheDocument();
});

it('should call onClose when the close button is clicked', () => {
const onClose = jest.fn();
render(<PdfViewerModal doc={mockDoc} onClose={onClose} />);

fireEvent.click(screen.getByRole('button', { name: /close/i }));
expect(onClose).toHaveBeenCalledTimes(1);
});

it('should render the modal and keep the close button available even on a failing document', () => {
const brokenDoc: PropertyDocument = { ...mockDoc, url: 'https://example.com/broken.pdf' };
const onClose = jest.fn();
render(<PdfViewerModal doc={brokenDoc} onClose={onClose} />);

// The modal shell stays mounted so the user can always dismiss it
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
expect(screen.getByTestId('pdf-document')).toHaveAttribute('data-fail', 'true');

fireEvent.click(screen.getByRole('button', { name: /close/i }));
expect(onClose).toHaveBeenCalledTimes(1);
});
});
Loading
Loading