217 lines
7.7 KiB
TypeScript
217 lines
7.7 KiB
TypeScript
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(
|
|
<ClubHierarchyBuilder
|
|
clubId="club-test"
|
|
ranks={DEFAULT_CLUB_RANKS}
|
|
members={mockMembers}
|
|
selectedRankId="president"
|
|
onSelectRank={handleSelectRank}
|
|
onUpdateMemberRank={handleUpdateMemberRank}
|
|
canManageRanks={true}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<ClubHierarchyBuilder
|
|
clubId="club-test"
|
|
ranks={DEFAULT_CLUB_RANKS}
|
|
members={mockMembers}
|
|
selectedRankId="president"
|
|
onSelectRank={handleSelectRank}
|
|
onUpdateMemberRank={handleUpdateMemberRank}
|
|
canManageRanks={true}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<ClubHierarchyBuilder
|
|
clubId="club-test"
|
|
ranks={DEFAULT_CLUB_RANKS}
|
|
members={mockMembers}
|
|
selectedRankId="road_captain"
|
|
onSelectRank={handleSelectRank}
|
|
onUpdateMemberRank={handleUpdateMemberRank}
|
|
canManageRanks={true}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<ClubPermissionMatrix
|
|
ranks={DEFAULT_CLUB_RANKS}
|
|
capabilities={DEFAULT_CLUB_CAPABILITIES}
|
|
permissions={DEFAULT_RANK_PERMISSIONS}
|
|
selectedRankId="road_captain"
|
|
onTogglePermission={handleToggle}
|
|
canEdit={true}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<ClubPermissionMatrix
|
|
ranks={DEFAULT_CLUB_RANKS}
|
|
capabilities={DEFAULT_CLUB_CAPABILITIES}
|
|
permissions={DEFAULT_RANK_PERMISSIONS}
|
|
selectedRankId="road_captain"
|
|
onTogglePermission={handleToggle}
|
|
onSavePermissions={handleSave}
|
|
canEdit={true}
|
|
// persistenceAvailable defaults to false
|
|
/>
|
|
);
|
|
|
|
// 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(
|
|
<ClubPermissionMatrix
|
|
ranks={DEFAULT_CLUB_RANKS}
|
|
capabilities={DEFAULT_CLUB_CAPABILITIES}
|
|
permissions={DEFAULT_RANK_PERMISSIONS}
|
|
selectedRankId="road_captain"
|
|
onTogglePermission={handleToggle}
|
|
onResetDefaults={handleReset}
|
|
onSavePermissions={handleSave}
|
|
isSaving={false}
|
|
canEdit={true}
|
|
persistenceAvailable={true}
|
|
/>
|
|
);
|
|
|
|
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();
|
|
});
|
|
});
|