MotorSignal-App/website/tests/group-rides-page.test.tsx

348 lines
15 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 { 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';
vi.mock('next/navigation', () => ({
usePathname: () => '/',
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn() }),
}));
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('activates radar lens camera and simulates QR scan successfully', async () => {
const onClose = vi.fn();
const onJoinSuccess = vi.fn();
render(<JoinRideModal isOpen={true} onClose={onClose} onJoinSuccess={onJoinSuccess} />);
// Click activate optical lens
const lensTrigger = screen.getByTestId('activate-radar-lens-button');
fireEvent.click(lensTrigger);
expect(screen.getByTestId('radar-lens-viewfinder')).toBeDefined();
// Click simulate QR scan
const simulateScanBtn = screen.getByRole('button', { name: /simulate qr scan/i });
fireEvent.click(simulateScanBtn);
await waitFor(() => {
expect(screen.getByText(/Telemetry Linked/i)).toBeDefined();
});
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
expect(onJoinSuccess).toHaveBeenCalledWith('APEX84', 'Bay Area Dawn Patrol');
});
});
// ============================================================
// 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. Full GroupRidesPage Integrated Flow
// ============================================================
describe('Full GroupRidesPage Integration', () => {
it('coordinates create ride and join ride modals and alerts', async () => {
render(<GroupRidesPage />);
// Hero check
expect(screen.getByText(/Ride in pack formation/i)).toBeDefined();
expect(screen.getByText(/Never lose a wingman/i)).toBeDefined();
// Open Create Modal from hero button
const createHeroBtn = screen.getByRole('button', { name: /create group ride/i });
fireEvent.click(createHeroBtn);
expect(screen.getByRole('heading', { name: /create group ride run/i })).toBeDefined();
// Fill and submit
fireEvent.change(screen.getByLabelText(/ride title/i), {
target: { value: 'Redwood Highway Sprint' },
});
fireEvent.change(screen.getByLabelText(/region \/ city/i), {
target: { value: 'Eureka, CA' },
});
fireEvent.click(screen.getByRole('button', { name: /publish group ride/i }));
// Directory should now show the new ride at the top
expect(screen.getByRole('heading', { name: /Redwood Highway Sprint/i })).toBeDefined();
expect(screen.getByText(/Eureka, CA/i)).toBeDefined();
// Open Join Modal from hero button
const joinHeroBtn = screen.getByRole('button', { name: /join with ride code \/ qr/i });
fireEvent.click(joinHeroBtn);
expect(screen.getByRole('heading', { name: /join pack formation/i })).toBeDefined();
// Enter code and submit
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(/Telemetry Linked/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();
});
});
});