mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
test: expand frontend test suite to 82% coverage
Adds ~45 new and updated test files covering Admin, Collab, Dashboard, Map, Memories, PDF, Photos, Planner, Settings, Vacay, Weather components, pages, stores, and a WebSocket integration test.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
// FE-COMP-TRIPFORM-001 to FE-COMP-TRIPFORM-015
|
||||
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
||||
// FE-COMP-TRIPFORM-001 to FE-COMP-TRIPFORM-028
|
||||
import { render, screen, waitFor, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useTripStore } from '../../store/tripStore';
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import { buildUser, buildTrip } from '../../../tests/helpers/factories';
|
||||
import { server } from '../../../tests/helpers/msw/server';
|
||||
import TripFormModal from './TripFormModal';
|
||||
|
||||
const defaultProps = {
|
||||
@@ -129,4 +131,159 @@ describe('TripFormModal', () => {
|
||||
expect(screen.getByText('Start Date')).toBeInTheDocument();
|
||||
expect(screen.getByText('End Date')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-016: end-date validation shows error when end < start', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSave = vi.fn();
|
||||
// Trip with end_date before start_date; title is set so title validation passes
|
||||
const trip = buildTrip({ id: 1, title: 'Test Trip', start_date: '2026-06-15', end_date: '2026-06-01' } as any);
|
||||
render(<TripFormModal {...defaultProps} trip={trip} onSave={onSave} />);
|
||||
const updateBtn = screen.getByRole('button', { name: /Update/i });
|
||||
await user.click(updateBtn);
|
||||
await screen.findByText('End date must be after start date');
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-017: day count field visible when no dates set', () => {
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
expect(screen.getByText('Number of Days')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-018: day count hidden when trip has dates', () => {
|
||||
const trip = buildTrip({ id: 1, start_date: '2026-06-01', end_date: '2026-06-10' });
|
||||
render(<TripFormModal {...defaultProps} trip={trip} />);
|
||||
expect(screen.queryByText('Number of Days')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-019: reminder buttons visible when tripRemindersEnabled=true', async () => {
|
||||
seedStore(useAuthStore, { tripRemindersEnabled: true });
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
expect(screen.getByRole('button', { name: 'None' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '1 day' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '3 days' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '9 days' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Custom' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-020: reminder section shows disabled hint when tripRemindersEnabled=false', () => {
|
||||
seedStore(useAuthStore, { tripRemindersEnabled: false });
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
expect(screen.getByText(/Trip reminders are disabled/i)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'None' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Custom' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-021: custom reminder input appears and accepts value', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(useAuthStore, { tripRemindersEnabled: true });
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
await user.click(screen.getByRole('button', { name: 'Custom' }));
|
||||
// custom reminder input has max=30
|
||||
const customInput = document.querySelector('input[max="30"]') as HTMLInputElement;
|
||||
expect(customInput).toBeInTheDocument();
|
||||
// Use fireEvent.change to set the value directly (avoids clamping from char-by-char typing)
|
||||
fireEvent.change(customInput, { target: { value: '14' } });
|
||||
expect(customInput.value).toBe('14');
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-022: member selector not visible when editing existing trip', () => {
|
||||
const trip = buildTrip({ id: 1 });
|
||||
render(<TripFormModal {...defaultProps} trip={trip} />);
|
||||
expect(screen.queryByText('Travel buddies')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-023: member selector appears when creating and other users exist', async () => {
|
||||
server.use(
|
||||
http.get('/api/auth/users', () =>
|
||||
HttpResponse.json({ users: [{ id: 100, username: 'alice' }] })
|
||||
)
|
||||
);
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
await screen.findByText('Travel buddies');
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-024: selecting a member adds a chip', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(useAuthStore, { user: buildUser({ id: 1, username: 'me' }), isAuthenticated: true });
|
||||
server.use(
|
||||
http.get('/api/auth/users', () =>
|
||||
HttpResponse.json({ users: [{ id: 100, username: 'alice' }] })
|
||||
)
|
||||
);
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
// Wait for member section to load
|
||||
await screen.findByText('Travel buddies');
|
||||
// Click the CustomSelect trigger (placeholder "Add member")
|
||||
const selectTrigger = screen.getByText('Add member').closest('button')!;
|
||||
await user.click(selectTrigger);
|
||||
// alice option appears in portal (document.body)
|
||||
const aliceOption = await screen.findByRole('button', { name: 'alice' });
|
||||
await user.click(aliceOption);
|
||||
// alice chip should now be in the member chip list
|
||||
expect(screen.getByText('alice')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-025: removing a member chip deselects them', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(useAuthStore, { user: buildUser({ id: 1, username: 'me' }), isAuthenticated: true });
|
||||
server.use(
|
||||
http.get('/api/auth/users', () =>
|
||||
HttpResponse.json({ users: [{ id: 100, username: 'alice' }] })
|
||||
)
|
||||
);
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
await screen.findByText('Travel buddies');
|
||||
// Select alice
|
||||
const selectTrigger = screen.getByText('Add member').closest('button')!;
|
||||
await user.click(selectTrigger);
|
||||
const aliceOption = await screen.findByRole('button', { name: 'alice' });
|
||||
await user.click(aliceOption);
|
||||
// alice chip is present
|
||||
const aliceChip = screen.getByText('alice');
|
||||
expect(aliceChip).toBeInTheDocument();
|
||||
// Click the chip to remove alice
|
||||
await user.click(aliceChip.closest('span')!);
|
||||
// alice chip should be gone
|
||||
await waitFor(() => expect(screen.queryByText('alice')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-026: cover image paste fires URL.createObjectURL', async () => {
|
||||
const mockCreateObjectURL = vi.fn(() => 'blob:mock-paste-url');
|
||||
const original = URL.createObjectURL;
|
||||
Object.defineProperty(URL, 'createObjectURL', { writable: true, configurable: true, value: mockCreateObjectURL });
|
||||
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
const form = document.querySelector('form')!;
|
||||
const file = new File(['img'], 'cover.png', { type: 'image/png' });
|
||||
fireEvent.paste(form, {
|
||||
clipboardData: {
|
||||
items: [{ type: 'image/png', getAsFile: () => file }],
|
||||
},
|
||||
});
|
||||
expect(mockCreateObjectURL).toHaveBeenCalledWith(file);
|
||||
|
||||
Object.defineProperty(URL, 'createObjectURL', { writable: true, configurable: true, value: original });
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-027: onSave error message is displayed', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSave = vi.fn().mockRejectedValue(new Error('Server error'));
|
||||
render(<TripFormModal {...defaultProps} onSave={onSave} trip={null} />);
|
||||
await user.type(screen.getByPlaceholderText(/Summer in Japan/i), 'My Trip');
|
||||
const submitBtns = screen.getAllByText('Create New Trip');
|
||||
const submitBtn = submitBtns.find(el => el.closest('button'))!;
|
||||
await user.click(submitBtn.closest('button')!);
|
||||
await screen.findByText('Server error');
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-028: loading spinner shown while submitting', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSave = vi.fn().mockImplementation(() => new Promise(() => {}));
|
||||
render(<TripFormModal {...defaultProps} onSave={onSave} trip={null} />);
|
||||
await user.type(screen.getByPlaceholderText(/Summer in Japan/i), 'My Trip');
|
||||
const submitBtns = screen.getAllByText('Create New Trip');
|
||||
const submitBtn = submitBtns.find(el => el.closest('button'))!;
|
||||
await user.click(submitBtn.closest('button')!);
|
||||
await waitFor(() => expect(screen.getByText('Saving...')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user