mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
test(front): add test suite frontend (WIP)
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { render, screen, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
|
||||
describe('ConfirmDialog', () => {
|
||||
const onClose = vi.fn();
|
||||
const onConfirm = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onClose.mockClear();
|
||||
onConfirm.mockClear();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-001: does not render when isOpen is false', () => {
|
||||
render(
|
||||
<ConfirmDialog isOpen={false} onClose={onClose} onConfirm={onConfirm} message="Are you sure?" />
|
||||
);
|
||||
expect(screen.queryByText('Are you sure?')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-002: renders with default title "Confirm" and message', () => {
|
||||
render(
|
||||
<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} message="Are you sure?" />
|
||||
);
|
||||
expect(screen.getByText('Confirm')).toBeTruthy();
|
||||
expect(screen.getByText('Are you sure?')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-003: renders custom title and message', () => {
|
||||
render(
|
||||
<ConfirmDialog
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title="Remove item"
|
||||
message="This cannot be undone."
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Remove item')).toBeTruthy();
|
||||
expect(screen.getByText('This cannot be undone.')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-004: Cancel button calls onClose', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} />);
|
||||
await user.click(screen.getByRole('button', { name: /cancel/i }));
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-005: Confirm button calls onConfirm and onClose', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} />);
|
||||
await user.click(screen.getByRole('button', { name: /delete/i }));
|
||||
expect(onConfirm).toHaveBeenCalledOnce();
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-006: custom button labels render correctly', () => {
|
||||
render(
|
||||
<ConfirmDialog
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
confirmLabel="Yes, remove"
|
||||
cancelLabel="Go back"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Yes, remove' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: 'Go back' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-007: Escape key calls onClose', () => {
|
||||
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} />);
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('FE-COMP-CONFIRM-008: clicking backdrop calls onClose', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ConfirmDialog isOpen={true} onClose={onClose} onConfirm={onConfirm} message="msg" />);
|
||||
// The outermost fixed div is the backdrop — click outside the card
|
||||
const backdrop = document.querySelector('.fixed') as HTMLElement;
|
||||
// fireEvent click on the backdrop element directly
|
||||
fireEvent.click(backdrop);
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { render, screen, fireEvent, act } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { Trash2, Edit } from 'lucide-react';
|
||||
|
||||
const makeMenu = (x = 100, y = 200, overrides?: object[]) => ({
|
||||
x,
|
||||
y,
|
||||
items: overrides ?? [
|
||||
{ label: 'Edit', icon: Edit, onClick: vi.fn() },
|
||||
{ label: 'Delete', icon: Trash2, onClick: vi.fn(), danger: true },
|
||||
],
|
||||
});
|
||||
|
||||
describe('ContextMenu', () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onClose.mockClear();
|
||||
});
|
||||
|
||||
it('FE-COMP-CTX-001: renders nothing when menu is null', () => {
|
||||
render(<ContextMenu menu={null} onClose={onClose} />);
|
||||
expect(document.body.querySelector('[style*="z-index: 999999"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-CTX-002: renders menu items at the specified position', () => {
|
||||
render(<ContextMenu menu={makeMenu(150, 250)} onClose={onClose} />);
|
||||
expect(screen.getByText('Edit')).toBeTruthy();
|
||||
expect(screen.getByText('Delete')).toBeTruthy();
|
||||
|
||||
// Portal root div has position fixed at the given coords
|
||||
const portal = document.body.querySelector('[style*="position: fixed"]') as HTMLElement;
|
||||
expect(portal.style.left).toBe('150px');
|
||||
expect(portal.style.top).toBe('250px');
|
||||
});
|
||||
|
||||
it('FE-COMP-CTX-003: clicking a menu item calls its onClick and onClose', async () => {
|
||||
const onClick = vi.fn();
|
||||
const menu = makeMenu(100, 200, [{ label: 'Copy', onClick }]);
|
||||
const user = userEvent.setup();
|
||||
render(<ContextMenu menu={menu} onClose={onClose} />);
|
||||
|
||||
await user.click(screen.getByText('Copy'));
|
||||
expect(onClick).toHaveBeenCalledOnce();
|
||||
// onClose is called once by the button handler and once by the document click listener
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-COMP-CTX-004: divider items render as a separator without text', () => {
|
||||
const menu = makeMenu(100, 200, [
|
||||
{ label: 'Item A', onClick: vi.fn() },
|
||||
{ divider: true },
|
||||
{ label: 'Item B', onClick: vi.fn() },
|
||||
]);
|
||||
render(<ContextMenu menu={menu} onClose={onClose} />);
|
||||
expect(screen.getByText('Item A')).toBeTruthy();
|
||||
expect(screen.getByText('Item B')).toBeTruthy();
|
||||
// Divider should not have any button text
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('FE-COMP-CTX-005: danger items have red color styling', () => {
|
||||
const menu = makeMenu(100, 200, [
|
||||
{ label: 'Remove', onClick: vi.fn(), danger: true },
|
||||
]);
|
||||
render(<ContextMenu menu={menu} onClose={onClose} />);
|
||||
const btn = screen.getByRole('button', { name: /remove/i });
|
||||
// Danger buttons use color #ef4444 inline style
|
||||
expect(btn.style.color).toBe('rgb(239, 68, 68)');
|
||||
});
|
||||
|
||||
it('FE-COMP-CTX-006: clicking outside the menu closes it via document click listener', () => {
|
||||
render(<ContextMenu menu={makeMenu()} onClose={onClose} />);
|
||||
// Document click event triggers the close handler
|
||||
act(() => {
|
||||
document.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { render, screen, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import CustomSelect from './CustomSelect';
|
||||
|
||||
const OPTIONS = [
|
||||
{ value: 'apple', label: 'Apple' },
|
||||
{ value: 'banana', label: 'Banana' },
|
||||
{ value: 'cherry', label: 'Cherry' },
|
||||
];
|
||||
|
||||
describe('CustomSelect', () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-001: renders placeholder when no value is selected', () => {
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} placeholder="Pick a fruit" />);
|
||||
expect(screen.getByText('Pick a fruit')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-002: renders the selected option label', () => {
|
||||
render(<CustomSelect value="banana" onChange={onChange} options={OPTIONS} placeholder="Pick" />);
|
||||
expect(screen.getByText('Banana')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-003: clicking trigger opens the dropdown', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} />);
|
||||
const trigger = screen.getByRole('button');
|
||||
await user.click(trigger);
|
||||
// All options should now be visible in the portal
|
||||
expect(screen.getByText('Apple')).toBeTruthy();
|
||||
expect(screen.getByText('Banana')).toBeTruthy();
|
||||
expect(screen.getByText('Cherry')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-004: options are displayed in the dropdown', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} />);
|
||||
await user.click(screen.getByRole('button'));
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(1); // trigger + option buttons
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-005: clicking an option calls onChange with correct value', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} />);
|
||||
await user.click(screen.getByRole('button')); // open
|
||||
// Options in dropdown are also buttons
|
||||
const optionBtns = screen.getAllByRole('button');
|
||||
// Find the Cherry option button (not the trigger which shows placeholder)
|
||||
const cherryBtn = optionBtns.find(b => b.textContent?.includes('Cherry'));
|
||||
await user.click(cherryBtn!);
|
||||
expect(onChange).toHaveBeenCalledWith('cherry');
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-006: clicking an option closes the dropdown', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} />);
|
||||
await user.click(screen.getByRole('button')); // open
|
||||
const optionBtns = screen.getAllByRole('button');
|
||||
const appleBtn = optionBtns.find(b => b.textContent?.includes('Apple'));
|
||||
await user.click(appleBtn!);
|
||||
// After selection, only the trigger button remains in DOM
|
||||
expect(screen.getAllByRole('button')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-007: searchable mode filters options by typed text', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} searchable={true} />);
|
||||
await user.click(screen.getByRole('button')); // open
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('...');
|
||||
await user.type(searchInput, 'ban');
|
||||
|
||||
// Only Banana should remain, Apple and Cherry should be filtered out
|
||||
expect(screen.getByText('Banana')).toBeTruthy();
|
||||
expect(screen.queryByText('Apple')).toBeNull();
|
||||
expect(screen.queryByText('Cherry')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-SELECT-008: disabled state prevents the dropdown from opening', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} disabled={true} placeholder="Pick" />);
|
||||
const trigger = screen.getByRole('button');
|
||||
await user.click(trigger);
|
||||
// Dropdown should not be in the DOM — options remain hidden
|
||||
expect(screen.queryByText('Apple')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { render, screen, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Modal from './Modal';
|
||||
|
||||
describe('Modal', () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onClose.mockClear();
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-001: does not render when isOpen is false', () => {
|
||||
render(<Modal isOpen={false} onClose={onClose}><p>content</p></Modal>);
|
||||
expect(screen.queryByText('content')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-002: renders overlay when isOpen is true', () => {
|
||||
render(<Modal isOpen={true} onClose={onClose}><p>content</p></Modal>);
|
||||
expect(screen.getByText('content')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-003: renders the title prop', () => {
|
||||
render(<Modal isOpen={true} onClose={onClose} title="My Modal Title" />);
|
||||
expect(screen.getByText('My Modal Title')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-004: renders children content', () => {
|
||||
render(<Modal isOpen={true} onClose={onClose}><p>Hello World</p></Modal>);
|
||||
expect(screen.getByText('Hello World')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-005: renders footer prop', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={onClose} footer={<button>Save</button>}>
|
||||
<p>body</p>
|
||||
</Modal>
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Save' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-006: close button calls onClose', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Modal isOpen={true} onClose={onClose} title="T" />);
|
||||
// The X button is the only button rendered by Modal itself
|
||||
const closeBtn = document.querySelector('button');
|
||||
await user.click(closeBtn!);
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-007: Escape key calls onClose', () => {
|
||||
render(<Modal isOpen={true} onClose={onClose} title="T" />);
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-008: clicking the backdrop calls onClose', () => {
|
||||
render(<Modal isOpen={true} onClose={onClose}><p>inner</p></Modal>);
|
||||
const backdrop = document.querySelector('.modal-backdrop') as HTMLElement;
|
||||
// Simulate mousedown then click on the backdrop itself
|
||||
fireEvent.mouseDown(backdrop, { target: backdrop });
|
||||
fireEvent.click(backdrop);
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-009: clicking inside modal content does NOT call onClose', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Modal isOpen={true} onClose={onClose}><p>inner content</p></Modal>);
|
||||
await user.click(screen.getByText('inner content'));
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-010: close button is hidden when hideCloseButton is true', () => {
|
||||
render(<Modal isOpen={true} onClose={onClose} title="T" hideCloseButton={true} />);
|
||||
// No button should be present in the modal header
|
||||
expect(document.querySelector('button')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-MODAL-011: sets document.body overflow to hidden when open', () => {
|
||||
render(<Modal isOpen={true} onClose={onClose} />);
|
||||
expect(document.body.style.overflow).toBe('hidden');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { render, screen, act } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ToastContainer } from './Toast';
|
||||
|
||||
describe('ToastContainer', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function addToast(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info', duration = 3000) {
|
||||
act(() => {
|
||||
window.__addToast!(message, type, duration);
|
||||
});
|
||||
}
|
||||
|
||||
it('FE-COMP-TOAST-001: renders empty container initially', () => {
|
||||
const { container } = render(<ToastContainer />);
|
||||
// No toast items — only the outer container div
|
||||
expect(container.querySelectorAll('.nomad-toast').length).toBe(0);
|
||||
});
|
||||
|
||||
it('FE-COMP-TOAST-002: success toast renders with message', () => {
|
||||
render(<ToastContainer />);
|
||||
addToast('File saved successfully', 'success');
|
||||
expect(screen.getByText('File saved successfully')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-TOAST-003: error toast renders with message', () => {
|
||||
render(<ToastContainer />);
|
||||
addToast('Something went wrong', 'error');
|
||||
expect(screen.getByText('Something went wrong')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-TOAST-004: warning toast renders with message', () => {
|
||||
render(<ToastContainer />);
|
||||
addToast('Low disk space', 'warning');
|
||||
expect(screen.getByText('Low disk space')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-TOAST-005: info toast renders with message', () => {
|
||||
render(<ToastContainer />);
|
||||
addToast('Update available', 'info');
|
||||
expect(screen.getByText('Update available')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-COMP-TOAST-006: toast auto-dismisses after duration', () => {
|
||||
render(<ToastContainer />);
|
||||
addToast('Temporary message', 'info', 2000);
|
||||
expect(screen.getByText('Temporary message')).toBeTruthy();
|
||||
|
||||
// After duration + 400ms animation delay, toast is removed
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2000 + 400 + 10);
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Temporary message')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-TOAST-007: clicking close button dismisses the toast', () => {
|
||||
const { container } = render(<ToastContainer />);
|
||||
act(() => {
|
||||
window.__addToast!('Close me', 'success', 0); // duration 0 = no auto-dismiss
|
||||
});
|
||||
|
||||
expect(screen.getByText('Close me')).toBeTruthy();
|
||||
|
||||
const closeBtn = container.querySelector('.nomad-toast button') as HTMLElement;
|
||||
act(() => {
|
||||
closeBtn.click();
|
||||
});
|
||||
|
||||
// removeToast sets removing: true then schedules removal after 400ms
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(401);
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Close me')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-COMP-TOAST-008: multiple toasts display simultaneously', () => {
|
||||
render(<ToastContainer />);
|
||||
addToast('First toast', 'success', 0);
|
||||
addToast('Second toast', 'error', 0);
|
||||
addToast('Third toast', 'info', 0);
|
||||
|
||||
expect(screen.getByText('First toast')).toBeTruthy();
|
||||
expect(screen.getByText('Second toast')).toBeTruthy();
|
||||
expect(screen.getByText('Third toast')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user