348 lines
15 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|
|
|