import { describe, it, expect, vi } from 'vitest'; import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { ClubHierarchyBuilder } from '../components/founder-portal/club-hierarchy-builder'; import { ClubPermissionMatrix } from '../components/founder-portal/club-permission-matrix'; import { DEFAULT_CLUB_RANKS, DEFAULT_CLUB_CAPABILITIES, DEFAULT_RANK_PERMISSIONS } from '../lib/club-data'; import { ClubMember } from '../components/founder-portal/types'; describe('WEB-13: ClubHierarchyBuilder & ClubPermissionMatrix', () => { const mockMembers: ClubMember[] = [ { uid: 'rider-01', clubId: 'club-test', status: 'active', role: 'owner', displayRank: 'President', displayName: 'Marcus Vance', handle: '@marcus_v', updatedAt: new Date().toISOString(), }, { uid: 'rider-02', clubId: 'club-test', status: 'active', role: 'admin', displayRank: 'Vice President', displayName: 'Elena Rostova', handle: '@elena_rides', updatedAt: new Date().toISOString(), }, { uid: 'rider-03', clubId: 'club-test', status: 'active', role: 'member', displayRank: 'Road Captain', displayName: 'Dave Miller', handle: '@dave_m', updatedAt: new Date().toISOString(), }, ]; it('renders all organizational tiers and rank nodes in hierarchy builder', () => { const handleSelectRank = vi.fn(); const handleUpdateMemberRank = vi.fn().mockResolvedValue(undefined); render( ); expect(screen.getByText('Club Hierarchy Builder')).toBeDefined(); expect(screen.getByText('Tier 1 • Executive Command')).toBeDefined(); expect(screen.getByText('Tier 2 • Leadership & Administration')).toBeDefined(); expect(screen.getByText('Tier 3 • Tactical Operations & Pack Safety')).toBeDefined(); expect(screen.getByText('Tier 4 • Squad Formation & Prospects')).toBeDefined(); expect(screen.getByText('President')).toBeDefined(); expect(screen.getByText('Vice President')).toBeDefined(); expect(screen.getByText('Road Captain')).toBeDefined(); expect(screen.getByText('Tail Gunner')).toBeDefined(); }); it('selects rank node and triggers onSelectRank callback', () => { const handleSelectRank = vi.fn(); const handleUpdateMemberRank = vi.fn().mockResolvedValue(undefined); render( ); const roadCaptainNode = screen.getByText('Road Captain'); fireEvent.click(roadCaptainNode); expect(handleSelectRank).toHaveBeenCalledWith('road_captain'); }); it('opens assign member modal and executes rank update', async () => { const handleSelectRank = vi.fn(); const handleUpdateMemberRank = vi.fn().mockResolvedValue(undefined); render( ); const assignButtons = screen.getAllByText('Assign Rider'); fireEvent.click(assignButtons[0]); expect(screen.getByText(/Assign to /)).toBeDefined(); const select = screen.getByRole('combobox'); fireEvent.change(select, { target: { value: 'rider-03' } }); const submitBtn = screen.getByRole('button', { name: /Assign Rank/i }); fireEvent.click(submitBtn); expect(handleUpdateMemberRank).toHaveBeenCalledWith('rider-03', expect.any(String)); }); it('renders permission matrix capabilities for reference', () => { const handleToggle = vi.fn(); render( ); expect(screen.getByText('Operational Permission Matrix')).toBeDefined(); expect(screen.getByText('Plan Club Rides')).toBeDefined(); expect(screen.getByText('Review Applications')).toBeDefined(); expect(screen.getByText('Post Announcements')).toBeDefined(); expect(screen.getByText('Moderate Club Wall')).toBeDefined(); }); // WEB-W1 regression: without a persistence backend the matrix must be an honest // read-only reference. It must not offer a Save control, must not report a save, // and must not mutate permissions — even for an Owner/Admin (canEdit=true). it('is read-only and never reports a false save when persistence is unavailable', () => { const handleToggle = vi.fn(); const handleSave = vi.fn().mockResolvedValue(undefined); render( ); // No save/reset affordances are rendered. expect(screen.queryByRole('button', { name: /Save Matrix/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Reset Defaults/i })).toBeNull(); expect(screen.queryByText(/Saved/)).toBeNull(); // A truthful view-only indicator and availability notice are present. expect(screen.getByText(/View-only reference/i)).toBeDefined(); expect(screen.getByText(/not saved yet/i)).toBeDefined(); // Toggles are disabled and do not mutate permissions. const toggleButton = screen.getByRole('button', { name: /Standard setting:\s*Plan Club Rides for Road Captain/i, }); expect((toggleButton as HTMLButtonElement).disabled).toBe(true); fireEvent.click(toggleButton); expect(handleToggle).not.toHaveBeenCalled(); expect(handleSave).not.toHaveBeenCalled(); }); // Forward-compatible editable path: when a persistence backend is wired AND the // caller may edit, the matrix exposes real Save/Reset controls and toggles. it('allows editing and saving only when persistence is available and canEdit', async () => { const handleToggle = vi.fn(); const handleReset = vi.fn(); const handleSave = vi.fn().mockResolvedValue(undefined); render( ); const toggleButton = screen.getByRole('button', { name: 'Toggle Plan Club Rides for Road Captain' }); fireEvent.click(toggleButton); expect(handleToggle).toHaveBeenCalledWith('road_captain', 'planRides'); const resetBtn = screen.getByRole('button', { name: /Reset Defaults/i }); fireEvent.click(resetBtn); expect(handleReset).toHaveBeenCalled(); const saveBtn = screen.getByRole('button', { name: /Save Matrix/i }); fireEvent.click(saveBtn); expect(handleSave).toHaveBeenCalled(); }); });