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,11 @@
|
||||
// FE-COMP-MEMBERS-001 to FE-COMP-MEMBERS-015
|
||||
// FE-COMP-MEMBERS-001 to FE-COMP-MEMBERS-025
|
||||
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../../tests/helpers/msw/server';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useTripStore } from '../../store/tripStore';
|
||||
import { usePermissionsStore } from '../../store/permissionsStore';
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import { buildUser, buildTrip } from '../../../tests/helpers/factories';
|
||||
import TripMembersModal from './TripMembersModal';
|
||||
@@ -172,4 +173,254 @@ describe('TripMembersModal', () => {
|
||||
render(<TripMembersModal {...defaultProps} isOpen={true} />);
|
||||
expect(screen.getByText('Share Trip')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ── Share Link Section (016-021) ───────────────────────────────────────────
|
||||
|
||||
it('FE-COMP-MEMBERS-016: share link section not rendered for non-owner', async () => {
|
||||
const nonOwner = buildUser({ id: 99, username: 'stranger' });
|
||||
seedStore(useAuthStore, { user: nonOwner, isAuthenticated: true });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: 1 }) });
|
||||
seedStore(usePermissionsStore, { permissions: { share_manage: 'trip_owner' } });
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
// Wait for members list to load so the component is fully rendered
|
||||
await screen.findByText(/Access/i);
|
||||
expect(screen.queryByText('Public Link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-017: share link section visible for owner', async () => {
|
||||
seedStore(usePermissionsStore, { permissions: { share_manage: 'trip_owner' } });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: ownerUser.id }) });
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('Public Link');
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-018: create share link shows URL after clicking create', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(usePermissionsStore, { permissions: { share_manage: 'trip_owner' } });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: ownerUser.id }) });
|
||||
|
||||
// GET returns null token initially; POST returns a new token
|
||||
server.use(
|
||||
http.get('/api/trips/1/share-link', () => HttpResponse.json({ token: null })),
|
||||
http.post('/api/trips/1/share-link', () =>
|
||||
HttpResponse.json({
|
||||
token: 'abc123',
|
||||
share_map: true,
|
||||
share_bookings: true,
|
||||
share_packing: false,
|
||||
share_budget: false,
|
||||
share_collab: false,
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
const createBtn = await screen.findByText('Create link');
|
||||
await user.click(createBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByDisplayValue(/\/shared\/abc123/);
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-019: copy share link calls clipboard.writeText', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(usePermissionsStore, { permissions: { share_manage: 'trip_owner' } });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: ownerUser.id }) });
|
||||
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText },
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get('/api/trips/1/share-link', () =>
|
||||
HttpResponse.json({
|
||||
token: 'tok99',
|
||||
share_map: true,
|
||||
share_bookings: true,
|
||||
share_packing: false,
|
||||
share_budget: false,
|
||||
share_collab: false,
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
const copyBtn = await screen.findByText('Copy');
|
||||
await user.click(copyBtn);
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('tok99'));
|
||||
await screen.findByText('Copied');
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-020: delete share link removes URL and shows create button', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(usePermissionsStore, { permissions: { share_manage: 'trip_owner' } });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: ownerUser.id }) });
|
||||
|
||||
let deleteHandlerCalled = false;
|
||||
server.use(
|
||||
http.get('/api/trips/1/share-link', () =>
|
||||
HttpResponse.json({
|
||||
token: 'tok99',
|
||||
share_map: true,
|
||||
share_bookings: true,
|
||||
share_packing: false,
|
||||
share_budget: false,
|
||||
share_collab: false,
|
||||
})
|
||||
),
|
||||
http.delete('/api/trips/1/share-link', () => {
|
||||
deleteHandlerCalled = true;
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
const deleteBtn = await screen.findByText('Delete link');
|
||||
await user.click(deleteBtn);
|
||||
|
||||
expect(deleteHandlerCalled).toBe(true);
|
||||
await screen.findByText('Create link');
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-021: clicking permission toggle calls POST with updated perms', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(usePermissionsStore, { permissions: { share_manage: 'trip_owner' } });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: ownerUser.id }) });
|
||||
|
||||
let postedPerms: Record<string, unknown> | null = null;
|
||||
server.use(
|
||||
http.get('/api/trips/1/share-link', () =>
|
||||
HttpResponse.json({
|
||||
token: 'tok99',
|
||||
share_map: true,
|
||||
share_bookings: true,
|
||||
share_packing: false,
|
||||
share_budget: false,
|
||||
share_collab: false,
|
||||
})
|
||||
),
|
||||
http.post('/api/trips/1/share-link', async ({ request }) => {
|
||||
postedPerms = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ token: 'tok99', ...postedPerms });
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
// Wait for the share section to load
|
||||
await screen.findByText('Public Link');
|
||||
// Click the "Packing" permission pill to toggle it on
|
||||
const packingBtn = await screen.findByText('Packing');
|
||||
await user.click(packingBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(postedPerms).not.toBeNull();
|
||||
expect(postedPerms).toMatchObject({ share_packing: true });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Member management (022-025) ────────────────────────────────────────────
|
||||
|
||||
it('FE-COMP-MEMBERS-022: adding a member via select + invite calls POST', async () => {
|
||||
const user = userEvent.setup();
|
||||
let postBody: Record<string, unknown> | null = null;
|
||||
server.use(
|
||||
http.post('/api/trips/1/members', async ({ request }) => {
|
||||
postBody = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
// Wait for Invite section to load
|
||||
await screen.findByText('Invite User');
|
||||
|
||||
// Open the CustomSelect by clicking its trigger button (shows placeholder)
|
||||
const selectTrigger = screen.getByText('Select user…');
|
||||
await user.click(selectTrigger);
|
||||
|
||||
// alice option appears in the portal dropdown
|
||||
const aliceOption = await screen.findByRole('button', { name: 'alice' });
|
||||
await user.click(aliceOption);
|
||||
|
||||
// Click Invite button
|
||||
const inviteBtn = screen.getByRole('button', { name: /Invite/i });
|
||||
await user.click(inviteBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(postBody).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-023: invite button is disabled when no user is selected', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('Invite User');
|
||||
|
||||
const inviteBtn = screen.getByRole('button', { name: /Invite/i });
|
||||
expect(inviteBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-024: leave trip calls DELETE for current user', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...window.location, reload: vi.fn() },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
seedStore(useAuthStore, { user: memberUser, isAuthenticated: true });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: ownerUser.id }) });
|
||||
|
||||
let deleteCalledForUserId: string | null = null;
|
||||
server.use(
|
||||
http.get('/api/trips/1/members', () =>
|
||||
HttpResponse.json({
|
||||
owner: { id: ownerUser.id, username: ownerUser.username, avatar_url: null },
|
||||
members: [{ id: memberUser.id, username: memberUser.username, avatar_url: null }],
|
||||
current_user_id: memberUser.id,
|
||||
})
|
||||
),
|
||||
http.delete('/api/trips/1/members/:userId', ({ params }) => {
|
||||
deleteCalledForUserId = params.userId as string;
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('alice');
|
||||
|
||||
const leaveBtn = screen.getByTitle('Leave trip');
|
||||
await user.click(leaveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteCalledForUserId).toBe(String(memberUser.id));
|
||||
});
|
||||
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-025: "all have access" message shown when all users are members', async () => {
|
||||
server.use(
|
||||
http.get('/api/trips/1/members', () =>
|
||||
HttpResponse.json({
|
||||
owner: { id: ownerUser.id, username: ownerUser.username, avatar_url: null },
|
||||
members: [{ id: memberUser.id, username: memberUser.username, avatar_url: null }],
|
||||
current_user_id: ownerUser.id,
|
||||
})
|
||||
),
|
||||
http.get('/api/auth/users', () =>
|
||||
HttpResponse.json({ users: [memberUser] })
|
||||
),
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('All users already have access.');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user