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() }), })); // Deterministic Firebase so the authenticated GroupRidesManagement hub renders and persists // without real network. `fb.commitImpl` lets individual tests force commit success/failure. const fb = vi.hoisted(() => ({ commitImpl: vi.fn(() => Promise.resolve()) })); vi.mock('firebase/firestore', () => ({ collection: vi.fn(() => ({})), query: vi.fn(() => ({})), where: vi.fn(() => ({})), onSnapshot: vi.fn(() => () => {}), doc: vi.fn((_fs: unknown, ...segs: string[]) => ({ path: segs.join('/') })), writeBatch: vi.fn(() => ({ set: vi.fn(), update: vi.fn(), commit: () => fb.commitImpl() })), updateDoc: vi.fn(() => Promise.resolve()), deleteDoc: vi.fn(() => Promise.resolve()), setDoc: vi.fn(() => Promise.resolve()), arrayUnion: vi.fn((x: unknown) => x), arrayRemove: vi.fn((x: unknown) => x), increment: vi.fn((n: unknown) => n), serverTimestamp: vi.fn(() => 'ts'), getDoc: vi.fn(() => Promise.resolve({ exists: () => false, data: () => ({}) })), getFirestore: vi.fn(() => ({})), connectFirestoreEmulator: vi.fn(), })); vi.mock('firebase/app', () => ({ getApps: vi.fn(() => []), initializeApp: vi.fn(() => ({ name: 'test' })), })); vi.mock('firebase/auth', () => ({ getAuth: vi.fn(() => ({ currentUser: null })), connectAuthEmulator: vi.fn(), signInWithEmailAndPassword: vi.fn(), signOut: vi.fn(() => Promise.resolve()), onAuthStateChanged: vi.fn((_auth: unknown, cb: (u: unknown) => void) => { cb(null); return () => {}; }), })); vi.mock('../lib/firebase-client', () => ({ firestore: {}, firebaseAuth: { currentUser: null } })); // An authenticated organizer: a real Firebase user always has a uid. const authedUser = { uid: 'organizer-uid-1', email: 'lead.rider@motorsignal.app', displayName: 'LeadRider' }; 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(); // Default: Firestore commit succeeds. Individual tests override for failure paths. fb.commitImpl.mockReset(); fb.commitImpl.mockResolvedValue(undefined); }); // ============================================================ // 1. LivePackRadar Component Tests // ============================================================ describe('LivePackRadar', () => { it('renders radar HUD, default tactical view, and all 6 rider telemetry cards', () => { render(); // 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(); 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(); // 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(); 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( ); expect(screen.queryByRole('dialog')).toBeNull(); rerender(); 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(); 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(); 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(); 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', async () => { const onClose = vi.fn(); const onCreateRide = vi.fn().mockResolvedValue(undefined); render(); 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', }) ); // The modal closes only after onCreateRide resolves (persistence confirmed). await waitFor(() => expect(onClose).toHaveBeenCalled()); }); it('disables the submit button while a create is in flight (no duplicate submissions)', async () => { const onClose = vi.fn(); let resolveCreate: () => void = () => {}; const onCreateRide = vi.fn(() => new Promise((r) => { resolveCreate = r; })); render(); fireEvent.change(screen.getByLabelText(/ride title/i), { target: { value: 'Pending Ride' } }); fireEvent.change(screen.getByLabelText(/region \/ city/i), { target: { value: 'Nowhere' } }); const submitBtn = screen.getByRole('button', { name: /publish group ride/i }); fireEvent.click(submitBtn); // While the create promise is pending, the button is disabled and relabeled. const pendingBtn = await screen.findByRole('button', { name: /publishing/i }); expect((pendingBtn as HTMLButtonElement).disabled).toBe(true); // A second click must not trigger another create. fireEvent.click(pendingBtn); expect(onCreateRide).toHaveBeenCalledTimes(1); resolveCreate(); await waitFor(() => expect(onClose).toHaveBeenCalled()); }); }); // ============================================================ // 4. GroupRidesDirectory Component Tests // ============================================================ describe('GroupRidesDirectory', () => { it('renders initial rides with correct pace tabs and badges', () => { render(); 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(); // 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(); 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(); 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(); // 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(); 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 () => { render(); // 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 })); // The ride is inserted only after Firestore confirms the write, so wait for it. await screen.findByRole('heading', { name: /High Sierra Alpine Sprint/i }); // 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(); }); it('blocks ride creation for an unauthenticated user and never shows a phantom ride', async () => { const anonUser = { email: 'anon@motorsignal.app' } as any; // no uid render(); fireEvent.click(screen.getByRole('button', { name: /create group ride/i })); fireEvent.change(screen.getByLabelText(/ride title/i), { target: { value: 'Phantom Ride' } }); fireEvent.change(screen.getByLabelText(/region \/ city/i), { target: { value: 'Nowhere' } }); fireEvent.click(screen.getByRole('button', { name: /publish group ride/i })); // Actionable error, and the ride must never appear in the list. await screen.findByText(/could not be saved/i); expect(screen.queryByRole('heading', { name: /Phantom Ride/i })).toBeNull(); }); it('does not leave a phantom ride when the Firestore commit fails', async () => { fb.commitImpl.mockReset(); fb.commitImpl.mockRejectedValue(new Error('offline')); render(); fireEvent.click(screen.getByRole('button', { name: /create group ride/i })); fireEvent.change(screen.getByLabelText(/ride title/i), { target: { value: 'Doomed Ride' } }); fireEvent.change(screen.getByLabelText(/region \/ city/i), { target: { value: 'Nowhere' } }); fireEvent.click(screen.getByRole('button', { name: /publish group ride/i })); await screen.findByText(/Could not create the ride/i); expect(screen.queryByRole('heading', { name: /Doomed Ride/i })).toBeNull(); }); }); describe('Full GroupRidesPage Smart Routing', () => { it('renders feature showcase for unauthenticated users', async () => { render(); 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(); 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(); await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/login?redirect=/group-rides/manage'); }); }); }); describe('GroupRidesShowcaseRoute (/group-rides/showcase)', () => { it('always renders showcase for visitors', async () => { render(); 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(); // 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(); 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(); }); }); });