test(front): add test suite frontend (WIP)

This commit is contained in:
jubnl
2026-04-07 12:31:09 +02:00
parent 96080e8a03
commit 3c31902885
97 changed files with 16973 additions and 4 deletions
@@ -0,0 +1,104 @@
import { render, screen, fireEvent, act } from '../../../tests/helpers/render';
// Mock photoService — all functions are no-ops / return null
vi.mock('../../services/photoService', () => ({
getCached: vi.fn(() => null),
isLoading: vi.fn(() => false),
fetchPhoto: vi.fn(),
onThumbReady: vi.fn(() => () => {}),
}));
// Mock IntersectionObserver as a class constructor
const mockDisconnect = vi.fn();
const mockObserve = vi.fn();
class MockIntersectionObserver {
callback: (entries: Partial<IntersectionObserverEntry>[]) => void;
constructor(callback: (entries: Partial<IntersectionObserverEntry>[]) => void) {
this.callback = callback;
}
observe = mockObserve;
disconnect = mockDisconnect;
unobserve = vi.fn();
}
beforeAll(() => {
(globalThis as any).IntersectionObserver = MockIntersectionObserver;
});
afterEach(() => {
mockDisconnect.mockClear();
mockObserve.mockClear();
});
import PlaceAvatar from './PlaceAvatar';
const basePlaceNoImage = {
id: 1,
name: 'Eiffel Tower',
image_url: null,
google_place_id: null,
osm_id: null,
lat: 48.8584,
lng: 2.2945,
};
const basePlaceWithImage = {
...basePlaceNoImage,
image_url: 'https://example.com/eiffel.jpg',
};
describe('PlaceAvatar', () => {
it('FE-COMP-AVATAR-001: renders an image when image_url is provided', () => {
render(<PlaceAvatar place={basePlaceWithImage} />);
const img = screen.getByRole('img');
expect(img).toBeTruthy();
expect((img as HTMLImageElement).src).toContain('eiffel.jpg');
});
it('FE-COMP-AVATAR-002: image has correct alt text equal to place.name', () => {
render(<PlaceAvatar place={basePlaceWithImage} />);
const img = screen.getByAltText('Eiffel Tower');
expect(img).toBeTruthy();
});
it('FE-COMP-AVATAR-003: renders an icon (no img) when no image_url', () => {
render(<PlaceAvatar place={basePlaceNoImage} />);
expect(screen.queryByRole('img')).toBeNull();
// The wrapper div should still be present
const { container } = render(<PlaceAvatar place={basePlaceNoImage} />);
expect(container.querySelector('div')).toBeTruthy();
});
it('FE-COMP-AVATAR-004: uses category color as background color', () => {
const { container } = render(
<PlaceAvatar place={basePlaceWithImage} category={{ color: '#ff5733', icon: 'MapPin' }} />
);
const wrapper = container.firstElementChild as HTMLElement;
expect(wrapper.style.backgroundColor).toBe('rgb(255, 87, 51)');
});
it('FE-COMP-AVATAR-005: uses default indigo color when no category provided', () => {
const { container } = render(<PlaceAvatar place={basePlaceWithImage} />);
const wrapper = container.firstElementChild as HTMLElement;
expect(wrapper.style.backgroundColor).toBe('rgb(99, 102, 241)');
});
it('FE-COMP-AVATAR-006: falls back to icon when image fails to load', () => {
render(<PlaceAvatar place={basePlaceWithImage} />);
const img = screen.getByRole('img');
// Simulate image load error
act(() => {
fireEvent.error(img);
});
// After error, img is removed and icon takes over
expect(screen.queryByRole('img')).toBeNull();
});
it('FE-COMP-AVATAR-007: respects the size prop for container dimensions', () => {
const { container } = render(<PlaceAvatar place={basePlaceWithImage} size={64} />);
const wrapper = container.firstElementChild as HTMLElement;
expect(wrapper.style.width).toBe('64px');
expect(wrapper.style.height).toBe('64px');
});
});