import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BoundingBox } from './index';
import type { BoundingBoxProps } from '@/types';
// Mock HTMLCanvasElement methods
beforeAll(() => {
HTMLCanvasElement.prototype.getContext = jest.fn(() => ({
clearRect: jest.fn(),
drawImage: jest.fn(),
getImageData: jest.fn(() => ({
data: new Uint8ClampedArray(400), // 10x10 image
width: 10,
height: 10,
})),
putImageData: jest.fn(),
strokeStyle: '',
lineWidth: 1,
beginPath: jest.fn(),
moveTo: jest.fn(),
lineTo: jest.fn(),
stroke: jest.fn(),
fillStyle: '',
font: '',
fillText: jest.fn(),
measureText: jest.fn(() => ({ width: 50 })),
fillRect: jest.fn(),
save: jest.fn(),
restore: jest.fn(),
scale: jest.fn(),
})) as any;
// Mock Image constructor
(global as any).Image = class {
constructor() {
setTimeout(() => {
if (this.onload) this.onload();
}, 100);
}
addEventListener = jest.fn();
removeEventListener = jest.fn();
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
src = '';
width = 100;
height = 100;
};
});
describe('BoundingBox Component', () => {
const defaultProps: BoundingBoxProps = {
image: 'test-image.jpg',
boxes: [
[10, 10, 50, 50],
[70, 70, 40, 40],
],
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('renders without crashing', () => {
render();
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
it('renders with custom className', () => {
const { container } = render(
);
expect(container.firstChild).toHaveClass('custom-class');
});
it('renders separate segmentation canvas when enabled', () => {
render(
);
expect(
screen.getByRole('img', { name: /segmentation overlay/i })
).toBeInTheDocument();
});
it('shows loading state initially', () => {
render();
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('displays error message when image fails to load', async () => {
// Mock failed image load
(global as any).Image = class {
constructor() {
setTimeout(() => {
if (this.onerror) this.onerror();
}, 100);
}
addEventListener = jest.fn();
removeEventListener = jest.fn();
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
src = '';
};
render();
await waitFor(() => {
expect(screen.getByText(/error loading image/i)).toBeInTheDocument();
});
});
});
describe('Box Interactions', () => {
it('calls onSelected when mouse moves over canvas', async () => {
const onSelected = jest.fn();
render();
const canvas = screen.getByRole('img', {
name: /image with bounding boxes/i,
});
// Wait for image to load
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
fireEvent.mouseMove(canvas, { clientX: 35, clientY: 35 });
await waitFor(() => {
expect(onSelected).toHaveBeenCalledWith(0);
});
});
it('calls onSelected with -1 when mouse leaves canvas', async () => {
const onSelected = jest.fn();
render();
const canvas = screen.getByRole('img', {
name: /image with bounding boxes/i,
});
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
fireEvent.mouseOut(canvas);
expect(onSelected).toHaveBeenCalledWith(-1);
});
it('handles click events on canvas', async () => {
const onSelected = jest.fn();
render();
const canvas = screen.getByRole('img', {
name: /image with bounding boxes/i,
});
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
fireEvent.click(canvas, { clientX: 35, clientY: 35 });
await waitFor(() => {
expect(onSelected).toHaveBeenCalled();
});
});
it('respects external selectedIndex prop', async () => {
const { rerender } = render(
);
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// Change selectedIndex
rerender();
// Component should sync with external state
// This would be tested through the hook's selectBox function
});
});
describe('Box Formats', () => {
it('handles array format boxes', () => {
const boxes = [
[10, 10, 50, 50],
[70, 70, 40, 40],
];
render();
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
it('handles min/max coordinate format', () => {
const boxes = [
{ xmin: 10, ymin: 10, xmax: 60, ymax: 60 },
{ xmin: 70, ymin: 70, xmax: 110, ymax: 110 },
];
render();
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
it('handles boxes with labels', () => {
const boxes = [
{ coord: [10, 10, 50, 50], label: 'Person' },
{ coord: [70, 70, 40, 40], label: 'Car' },
];
render();
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
});
describe('Segmentation', () => {
it('handles pixel segmentation data', () => {
const segmentationData = Array.from({ length: 100 }, (_, i) => i % 5);
render(
);
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
it('loads segmentation from JSON URL', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve({
body: {
predictions: [
{
vals: [1, 2, 3, 4, 5],
},
],
},
}),
})
) as jest.Mock;
render(
);
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith('/test-segmentation.json');
});
});
it('handles segmentation masks', () => {
const masks = [
{ width: 10, height: 10, data: Array.from({ length: 100 }, () => 1) },
];
const boxes = [{ xmin: 10, ymin: 10, xmax: 20, ymax: 20 }];
render(
);
expect(
screen.getByRole('img', { name: /segmentation overlay/i })
).toBeInTheDocument();
});
});
describe('Options and Styling', () => {
it('applies custom colors', () => {
const customOptions = {
colors: {
normal: 'red',
selected: 'green',
unselected: 'blue',
},
};
render();
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
it('applies custom styles', () => {
const customOptions = {
style: { maxWidth: '500px', border: '1px solid red' },
};
render();
const canvas = screen.getByRole('img', {
name: /image with bounding boxes/i,
});
expect(canvas).toHaveStyle({ maxWidth: '500px' });
});
it('handles base64 images', () => {
const base64Image =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
render(
);
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
});
describe('Performance', () => {
it('handles large number of boxes efficiently', async () => {
const largeBoundingBoxes = Array.from({ length: 1000 }, (_, i) => [
(i % 100) * 10,
Math.floor(i / 100) * 10,
50,
50,
]);
const start = performance.now();
render();
const end = performance.now();
expect(end - start).toBeLessThan(100); // Should render in under 100ms
});
it('does not cause memory leaks with frequent re-renders', async () => {
const { rerender } = render();
// Simulate frequent updates
for (let i = 0; i < 100; i++) {
rerender();
}
// Should complete without issues
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
});
describe('Error Handling', () => {
it('handles invalid box coordinates gracefully', () => {
const invalidBoxes = [
[NaN, 10, 50, 50],
[10, NaN, 50, 50],
null,
undefined,
'invalid',
] as any;
render();
// Should not crash
expect(
screen.getByRole('img', { name: /image with bounding boxes/i })
).toBeInTheDocument();
});
it('handles missing required props gracefully', () => {
// @ts-expect-error Testing invalid props
render();
// Should render with error or default state
expect(screen.getByText(/error loading image/i)).toBeInTheDocument();
});
});
});