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();
});
});
});