467 lines
21 KiB
TypeScript
467 lines
21 KiB
TypeScript
import React from 'react';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import GroupRidesPage from '../app/group-rides/page';
|
|
import { GroupRidesShowcase } from '../components/group-rides/group-rides-showcase';
|
|
import { GroupRidesManagement } from '../components/group-rides/group-rides-management';
|
|
import { LivePackRadar } from '../components/group-rides/live-pack-radar';
|
|
import { JoinRideModal } from '../components/group-rides/join-ride-modal';
|
|
import { CreateRideModal } from '../components/group-rides/create-ride-modal';
|
|
import { GroupRidesDirectory } from '../components/group-rides/group-rides-directory';
|
|
import { INITIAL_GROUP_RIDES } from '../components/group-rides/fixtures';
|
|
import { SiteHeaderClient } from '../components/site-header-client';
|
|
|
|
const mockPush = vi.fn();
|
|
const mockReplace = vi.fn();
|
|
|
|
vi.mock('next/navigation', () => ({
|
|
usePathname: () => '/',
|
|
useRouter: () => ({ push: mockPush, replace: mockReplace, back: vi.fn() }),
|
|
}));
|
|
|
|
import GroupRidesManageRoute from '../app/group-rides/manage/page';
|
|
import GroupRidesShowcaseRoute from '../app/group-rides/showcase/page';
|
|
|
|
describe('Group Rides Interactive Page & Components', () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// ============================================================
|
|
// 1. LivePackRadar Component Tests
|
|
// ============================================================
|
|
describe('LivePackRadar', () => {
|
|
it('renders radar HUD, default tactical view, and all 6 rider telemetry cards', () => {
|
|
render(<LivePackRadar initialMode="tactical" />);
|
|
|
|
// Verify header and HUD
|
|
expect(screen.getByText(/Swarm Recon: Skyline Blvd > Coast Hwy/i)).toBeDefined();
|
|
expect(screen.getByText(/Session #CA-84-SW/i)).toBeDefined();
|
|
expect(screen.getByText(/6 Synchronized/i)).toBeDefined();
|
|
|
|
// Verify GPS Frequency is NOT present, but GNSS Multi-Band Mesh IS present
|
|
expect(screen.queryByText(/GPS Frequency/i)).toBeNull();
|
|
expect(screen.getByText(/GNSS Multi-Band Mesh/i)).toBeDefined();
|
|
expect(screen.getByText(/18 Satellites Locked/i)).toBeDefined();
|
|
|
|
// Verify all riders in the stream
|
|
expect(screen.getByTestId('telemetry-card-ApexLead')).toBeDefined();
|
|
expect(screen.getByTestId('telemetry-card-Ghost99')).toBeDefined();
|
|
expect(screen.getByTestId('telemetry-card-Sarah_RR')).toBeDefined();
|
|
expect(screen.getByTestId('telemetry-card-Dan_KTM')).toBeDefined();
|
|
expect(screen.getByTestId('telemetry-card-Alex_Ducati')).toBeDefined();
|
|
expect(screen.getByTestId('telemetry-card-Sweep_Marshal')).toBeDefined();
|
|
|
|
// Verify alert text
|
|
expect(screen.getByText(/Rear tire 28 PSI \(low\)/i)).toBeDefined();
|
|
expect(screen.getByText(/Hard Braking \(-1.2G\) ABS Engaged/i)).toBeDefined();
|
|
});
|
|
|
|
it('allows toggling between Tactical, Satellite, and Elevation 3D modes', () => {
|
|
render(<LivePackRadar initialMode="tactical" />);
|
|
|
|
const tacticalBtn = screen.getByRole('button', { name: /^tactical$/i });
|
|
const satelliteBtn = screen.getByRole('button', { name: /^satellite$/i });
|
|
const elevationBtn = screen.getByRole('button', { name: /^elevation 3d$/i });
|
|
|
|
// Click Satellite
|
|
fireEvent.click(satelliteBtn);
|
|
expect(screen.getByTestId('satellite-mode-view')).toBeDefined();
|
|
expect(screen.getByText(/High-Resolution Orbit Imagery/i)).toBeDefined();
|
|
|
|
// Click Elevation 3D
|
|
fireEvent.click(elevationBtn);
|
|
expect(screen.getByTestId('elevation-mode-view')).toBeDefined();
|
|
expect(screen.getByText(/Summit: 2,210 ft/i)).toBeDefined();
|
|
|
|
// Return to Tactical
|
|
fireEvent.click(tacticalBtn);
|
|
expect(screen.queryByTestId('satellite-mode-view')).toBeNull();
|
|
expect(screen.queryByTestId('elevation-mode-view')).toBeNull();
|
|
});
|
|
|
|
it('allows selecting riders from telemetry column or canvas markers', () => {
|
|
render(<LivePackRadar initialMode="tactical" />);
|
|
|
|
// Default selected is ApexLead
|
|
expect(screen.getByText(/Selected Node Inspector/i)).toBeDefined();
|
|
|
|
// Select Alex_Ducati via telemetry card
|
|
const alexCard = screen.getByTestId('telemetry-card-Alex_Ducati');
|
|
fireEvent.click(alexCard);
|
|
|
|
// Verify inspector reflects Alex_Ducati
|
|
const inspector = screen.getByTestId('selected-node-inspector');
|
|
expect(inspector.textContent).toContain('Alex_Ducati');
|
|
expect(inspector.textContent).toContain('Decel Event');
|
|
|
|
// Select Ghost99 via canvas marker
|
|
const ghostMarker = screen.getByTestId('radar-marker-Ghost99');
|
|
fireEvent.click(ghostMarker);
|
|
expect(inspector.textContent).toContain('Ghost99');
|
|
});
|
|
|
|
it('animates/wobbles speed on ping swarm button click', () => {
|
|
render(<LivePackRadar initialMode="tactical" />);
|
|
const pingBtn = screen.getByRole('button', { name: /ping swarm/i });
|
|
fireEvent.click(pingBtn);
|
|
// Still renders active telemetry correctly
|
|
expect(screen.getByText(/6 Synchronized/i)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// 2. JoinRideModal Component Tests
|
|
// ============================================================
|
|
describe('JoinRideModal', () => {
|
|
it('handles open/close lifecycle and Escape key', () => {
|
|
const onClose = vi.fn();
|
|
const onJoinSuccess = vi.fn();
|
|
|
|
const { rerender } = render(
|
|
<JoinRideModal isOpen={false} onClose={onClose} onJoinSuccess={onJoinSuccess} />
|
|
);
|
|
expect(screen.queryByRole('dialog')).toBeNull();
|
|
|
|
rerender(<JoinRideModal isOpen={true} onClose={onClose} onJoinSuccess={onJoinSuccess} />);
|
|
expect(screen.getByRole('dialog')).toBeDefined();
|
|
|
|
fireEvent.keyDown(window, { key: 'Escape' });
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
|
|
it('validates short ride code and rejects submission with error', () => {
|
|
const onClose = vi.fn();
|
|
const onJoinSuccess = vi.fn();
|
|
|
|
render(<JoinRideModal isOpen={true} onClose={onClose} onJoinSuccess={onJoinSuccess} />);
|
|
|
|
const submitBtn = screen.getByRole('button', { name: /connect squad telemetry/i });
|
|
fireEvent.click(submitBtn);
|
|
|
|
expect(screen.getByText(/Please enter a valid ride code/i)).toBeDefined();
|
|
expect(onJoinSuccess).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('joins only after the ride code resolves successfully', async () => {
|
|
const onClose = vi.fn();
|
|
const onJoinSuccess = vi.fn().mockResolvedValue({
|
|
title: 'Saturday Canyon Ride', lead: 'Road Captain', status: 'requested',
|
|
});
|
|
|
|
render(<JoinRideModal isOpen={true} onClose={onClose} onJoinSuccess={onJoinSuccess} />);
|
|
fireEvent.change(screen.getByLabelText('Ride Code'), { target: { value: 'RIDE-123' } });
|
|
fireEvent.click(screen.getByRole('button', { name: /connect squad telemetry/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Request Sent/i)).toBeDefined();
|
|
});
|
|
expect(screen.getByText(/Saturday Canyon Ride/i)).toBeDefined();
|
|
expect(onJoinSuccess).toHaveBeenCalledWith('RIDE-123');
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// 3. CreateRideModal Component Tests
|
|
// ============================================================
|
|
describe('CreateRideModal', () => {
|
|
it('validates required fields before submitting', () => {
|
|
const onClose = vi.fn();
|
|
const onCreateRide = vi.fn();
|
|
|
|
render(<CreateRideModal isOpen={true} onClose={onClose} onCreateRide={onCreateRide} />);
|
|
|
|
const submitBtn = screen.getByRole('button', { name: /publish group ride/i });
|
|
fireEvent.click(submitBtn);
|
|
|
|
expect(screen.getByText(/Please enter a ride title/i)).toBeDefined();
|
|
expect(onCreateRide).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('submits valid ride and invokes onCreateRide callback', () => {
|
|
const onClose = vi.fn();
|
|
const onCreateRide = vi.fn();
|
|
|
|
render(<CreateRideModal isOpen={true} onClose={onClose} onCreateRide={onCreateRide} />);
|
|
|
|
const titleInput = screen.getByLabelText(/ride title/i);
|
|
const regionInput = screen.getByLabelText(/region \/ city/i);
|
|
|
|
fireEvent.change(titleInput, { target: { value: 'Skyline Sunrise Blast' } });
|
|
fireEvent.change(regionInput, { target: { value: 'Palo Alto, CA' } });
|
|
|
|
const submitBtn = screen.getByRole('button', { name: /publish group ride/i });
|
|
fireEvent.click(submitBtn);
|
|
|
|
expect(onCreateRide).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
title: 'Skyline Sunrise Blast',
|
|
region: 'Palo Alto, CA',
|
|
userStatus: 'joined',
|
|
})
|
|
);
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// 4. GroupRidesDirectory Component Tests
|
|
// ============================================================
|
|
describe('GroupRidesDirectory', () => {
|
|
it('renders initial rides with correct pace tabs and badges', () => {
|
|
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={vi.fn()} />);
|
|
|
|
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
|
|
expect(screen.getByText(/Malibu Canyons Fast Pace/i)).toBeDefined();
|
|
expect(screen.getByText(/Tail of the Dragon Run/i)).toBeDefined();
|
|
|
|
// Pace tabs
|
|
expect(screen.getByRole('button', { name: /all runs/i })).toBeDefined();
|
|
expect(screen.getByRole('button', { name: /brisk pace/i })).toBeDefined();
|
|
expect(screen.getByRole('button', { name: /track \/ apex/i })).toBeDefined();
|
|
expect(screen.getByRole('button', { name: /weekend tours/i })).toBeDefined();
|
|
});
|
|
|
|
it('filters rides by pace tab', () => {
|
|
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={vi.fn()} />);
|
|
|
|
// Click Track / Apex
|
|
const trackBtn = screen.getByRole('button', { name: /track \/ apex/i });
|
|
fireEvent.click(trackBtn);
|
|
|
|
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
|
|
expect(screen.queryByText(/Malibu Canyons Fast Pace/i)).toBeNull();
|
|
});
|
|
|
|
it('filters rides by search text and shows empty state when no match', () => {
|
|
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={vi.fn()} />);
|
|
|
|
const searchInput = screen.getByPlaceholderText(/search by region, canyon/i);
|
|
fireEvent.change(searchInput, { target: { value: 'Robbinsville' } });
|
|
|
|
expect(screen.getByText(/Tail of the Dragon Run/i)).toBeDefined();
|
|
expect(screen.queryByText(/Bay Area Dawn Patrol/i)).toBeNull();
|
|
|
|
// Non-matching search
|
|
fireEvent.change(searchInput, { target: { value: 'nonexistent-canyon-run' } });
|
|
expect(screen.getByText(/No Matching Pack Runs/i)).toBeDefined();
|
|
|
|
// Reset button restores
|
|
const resetBtn = screen.getByRole('button', { name: /reset filters/i });
|
|
fireEvent.click(resetBtn);
|
|
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
|
|
});
|
|
|
|
it('handles requesting a slot', () => {
|
|
const onRequestSlot = vi.fn();
|
|
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={onRequestSlot} />);
|
|
|
|
const requestButtons = screen.getAllByRole('button', { name: /request pack slot/i });
|
|
expect(requestButtons.length).toBeGreaterThan(0);
|
|
|
|
fireEvent.click(requestButtons[0]);
|
|
expect(onRequestSlot).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// 5. Separate Pages: Showcase (Logged Out) vs Management (Logged In)
|
|
// ============================================================
|
|
describe('GroupRidesShowcase (For Not Logged In Users)', () => {
|
|
it('renders hero, radar cockpit, 6-feature grid, and conversion CTAs', () => {
|
|
render(<GroupRidesShowcase isLoggedIn={false} />);
|
|
|
|
// Hero check
|
|
expect(screen.getByText(/Ride in pack formation/i)).toBeDefined();
|
|
expect(screen.getByText(/Never lose a wingman/i)).toBeDefined();
|
|
|
|
// Feature highlights check (6-feature grid)
|
|
expect(screen.getByText(/Staggered Paceline Warnings/i)).toBeDefined();
|
|
expect(screen.getByText(/Swarm Broadcast & Crash Separation/i)).toBeDefined();
|
|
expect(screen.getByText(/Peer-to-Peer Mesh Sync/i)).toBeDefined();
|
|
expect(screen.getByText(/3D Multi-Bike Replay & Debrief/i)).toBeDefined();
|
|
expect(screen.getByText(/Multi-Band GNSS Precision/i)).toBeDefined();
|
|
expect(screen.getByText(/Offline Cell-Tower Fallback/i)).toBeDefined();
|
|
|
|
// Hardware strip
|
|
expect(screen.getByText(/Cardo DMC & Sena Mesh/i)).toBeDefined();
|
|
expect(screen.getAllByText(/Bluetooth LE/i).length).toBeGreaterThan(0);
|
|
expect(screen.getAllByText(/Android Auto \/ CarPlay/i).length).toBeGreaterThan(0);
|
|
|
|
// CTAs check
|
|
expect(screen.getByRole('link', { name: /get started free/i })).toBeDefined();
|
|
expect(screen.getByRole('link', { name: /download app/i })).toBeDefined();
|
|
expect(screen.getAllByRole('link', { name: /sign in to join pack/i }).length).toBeGreaterThan(0);
|
|
|
|
// Community preview
|
|
expect(screen.getByText(/Sample Pack Formations/i)).toBeDefined();
|
|
});
|
|
|
|
it('renders logged-in banner when an authenticated user visits showcase', () => {
|
|
render(<GroupRidesShowcase isLoggedIn={true} userEmail="rider@motorsignal.app" />);
|
|
expect(screen.getByText(/You're viewing the public showcase\./i)).toBeDefined();
|
|
const returnBtn = screen.getByRole('link', { name: /return to group rides console/i });
|
|
expect(returnBtn).toBeDefined();
|
|
expect(returnBtn.getAttribute('href')).toBe('/group-rides/manage');
|
|
});
|
|
});
|
|
|
|
describe('GroupRidesManagement (For Logged In Users - View, Add, Remove, Alter)', () => {
|
|
it('allows viewing, adding, altering, canceling, and deleting group rides', async () => {
|
|
const mockUser = { email: 'lead.rider@motorsignal.app', displayName: 'LeadRider' };
|
|
render(<GroupRidesManagement currentUser={mockUser} />);
|
|
|
|
// Header & Console title
|
|
expect(screen.getByText(/Group Rides Management/i)).toBeDefined();
|
|
expect(screen.getByText(/Squad Management & Telemetry Hub/i)).toBeDefined();
|
|
|
|
// KPI Stat Cards check
|
|
expect(screen.getByText(/Active Squads Nearby/i)).toBeDefined();
|
|
expect(screen.getByText(/Total Miles in Formation/i)).toBeDefined();
|
|
expect(screen.getByText(/Pack Safety Score/i)).toBeDefined();
|
|
expect(screen.getByText(/Mesh Node Health/i)).toBeDefined();
|
|
|
|
// 1. VIEW: Verify initial organized and joined rides
|
|
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
|
|
expect(screen.getByText(/Road Captain \(Organizer\)/i)).toBeDefined();
|
|
expect(screen.getByText(/Malibu Canyons Fast Pace/i)).toBeDefined();
|
|
expect(screen.getByText(/Squad Wingman/i)).toBeDefined();
|
|
|
|
// Details Modal inspection
|
|
const detailBtns = screen.getAllByRole('button', { name: /details/i });
|
|
fireEvent.click(detailBtns[0]);
|
|
expect(screen.getByRole('dialog', { name: /bay area dawn patrol/i })).toBeDefined();
|
|
const closeDetailBtn = screen.getByRole('button', { name: /^close$/i });
|
|
fireEvent.click(closeDetailBtn);
|
|
expect(screen.queryByRole('dialog')).toBeNull();
|
|
|
|
// 2. ADD: Create a new group ride
|
|
const createBtn = screen.getByRole('button', { name: /create group ride/i });
|
|
fireEvent.click(createBtn);
|
|
|
|
expect(screen.getByRole('heading', { name: /create group ride run/i })).toBeDefined();
|
|
fireEvent.change(screen.getByLabelText(/ride title/i), {
|
|
target: { value: 'High Sierra Alpine Sprint' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText(/region \/ city/i), {
|
|
target: { value: 'Mammoth Lakes, CA' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: /publish group ride/i }));
|
|
|
|
// Verify newly created ride appears in the list
|
|
expect(screen.getByRole('heading', { name: /High Sierra Alpine Sprint/i })).toBeDefined();
|
|
|
|
// 3. ALTER: Edit existing organized ride parameters
|
|
const alterButtons = screen.getAllByRole('button', { name: /alter \/ edit/i });
|
|
fireEvent.click(alterButtons[0]);
|
|
|
|
expect(screen.getByRole('heading', { name: /alter group ride parameters/i })).toBeDefined();
|
|
const editTitleInput = screen.getByLabelText(/ride title \*/i);
|
|
fireEvent.change(editTitleInput, { target: { value: 'High Sierra Dawn Express' } });
|
|
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
|
|
|
|
expect(screen.getByRole('heading', { name: /High Sierra Dawn Express/i })).toBeDefined();
|
|
|
|
// 4. CANCEL: Mark ride as canceled
|
|
const cancelButtons = screen.getAllByRole('button', { name: /cancel/i });
|
|
// Click cancel on the first active ride
|
|
fireEvent.click(cancelButtons[0]);
|
|
expect(screen.getAllByText(/Canceled/i).length).toBeGreaterThan(0);
|
|
|
|
// 5. REMOVE / DELETE: Delete organized ride with confirmation modal
|
|
const deleteButtons = screen.getAllByRole('button', { name: /delete/i });
|
|
fireEvent.click(deleteButtons[0]);
|
|
|
|
expect(screen.getByRole('heading', { name: /delete group ride/i })).toBeDefined();
|
|
fireEvent.click(screen.getByRole('button', { name: /delete ride/i }));
|
|
|
|
// 6. A nonexistent code must not produce a simulated successful join.
|
|
const joinBtn = screen.getByRole('button', { name: /join with code/i });
|
|
fireEvent.click(joinBtn);
|
|
|
|
expect(screen.getByRole('heading', { name: /join pack formation/i })).toBeDefined();
|
|
const codeInput = screen.getByLabelText(/ride code/i);
|
|
fireEvent.change(codeInput, { target: { value: 'APEX84' } });
|
|
fireEvent.click(screen.getByRole('button', { name: /connect squad telemetry/i }));
|
|
|
|
await waitFor(() => expect(screen.getByText(/No public ride was found/i)).toBeDefined());
|
|
fireEvent.click(screen.getByRole('button', { name: /close modal/i }));
|
|
|
|
// 7. RADAR TAB:
|
|
const radarTabBtn = screen.getByRole('button', { name: /live squad radar cockpit/i });
|
|
fireEvent.click(radarTabBtn);
|
|
|
|
expect(screen.getByText(/Live Squad Telemetry Stream: Connected/i)).toBeDefined();
|
|
expect(screen.getByText(/Formation session #CA-84-SW/i)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Full GroupRidesPage Smart Routing', () => {
|
|
it('renders feature showcase for unauthenticated users', async () => {
|
|
render(<GroupRidesPage />);
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Ride in pack formation/i)).toBeDefined();
|
|
});
|
|
expect(screen.getByText(/Never lose a wingman/i)).toBeDefined();
|
|
});
|
|
|
|
it('renders group rides management console for authenticated users', () => {
|
|
const mockUser = { email: 'lead.rider@motorsignal.app', uid: 'test-user-123' } as any;
|
|
render(<GroupRidesManagement currentUser={mockUser} />);
|
|
expect(screen.getByText(/Group Rides Management/i)).toBeDefined();
|
|
expect(screen.getByText(/Squad Management & Telemetry Hub/i)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('GroupRidesManageRoute (/group-rides/manage)', () => {
|
|
it('redirects unauthenticated users immediately to /login?redirect=/group-rides/manage', async () => {
|
|
render(<GroupRidesManageRoute />);
|
|
await waitFor(() => {
|
|
expect(mockReplace).toHaveBeenCalledWith('/login?redirect=/group-rides/manage');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('GroupRidesShowcaseRoute (/group-rides/showcase)', () => {
|
|
it('always renders showcase for visitors', async () => {
|
|
render(<GroupRidesShowcaseRoute />);
|
|
expect(screen.getByText(/Ride in pack formation/i)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// 6. Authenticated Navigation to Group Rides
|
|
// ============================================================
|
|
describe('Authenticated Navigation to Group Rides', () => {
|
|
it('renders Group Rides quick button and user dropdown link for signed-in users', () => {
|
|
render(<SiteHeaderClient signedIn={true} role="rider" />);
|
|
|
|
// Quick header button for signed-in users
|
|
const quickGroupRidesBtn = screen.getByTitle('Group Rides Hub');
|
|
expect(quickGroupRidesBtn).toBeDefined();
|
|
expect(quickGroupRidesBtn.getAttribute('href')).toBe('/group-rides');
|
|
|
|
// Open Rider Menu dropdown
|
|
const riderMenuBtn = screen.getByRole('button', { name: /open rider menu/i });
|
|
fireEvent.click(riderMenuBtn);
|
|
|
|
// Verify Group Rides link exists inside dropdown menu
|
|
const groupRidesLinks = screen.getAllByRole('menuitem', { name: /group rides/i });
|
|
expect(groupRidesLinks.length).toBeGreaterThan(0);
|
|
expect(groupRidesLinks[0].getAttribute('href')).toBe('/group-rides');
|
|
});
|
|
|
|
it('renders Group Rides in mobile navigation drawer for signed-in users', () => {
|
|
render(<SiteHeaderClient signedIn={true} role="rider" />);
|
|
|
|
const hamburgerBtn = screen.getByRole('button', { name: /open navigation menu/i });
|
|
fireEvent.click(hamburgerBtn);
|
|
|
|
const groupRidesNavLinks = screen.getAllByRole('link', { name: /group rides/i });
|
|
const mobileDrawerLink = groupRidesNavLinks.find(link => link.getAttribute('href') === '/group-rides');
|
|
expect(mobileDrawerLink).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|