import { describe, it, expect, vi, beforeEach } from 'vitest' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { render } from '../../../tests/helpers/render' import { resetAllStores, seedStore } from '../../../tests/helpers/store' import { useVacayStore } from '../../store/vacayStore' import { useAuthStore } from '../../store/authStore' import { server } from '../../../tests/helpers/msw/server' import { http, HttpResponse } from 'msw' import VacayPersons from './VacayPersons' // ── MSW handler helpers ─────────────────────────────────────────────────────── function withAvailableUsers() { server.use( http.get('/api/addons/vacay/available-users', () => HttpResponse.json({ users: [{ id: 2, username: 'Bob', email: 'bob@example.com' }] }) ) ) } function withNoAvailableUsers() { server.use( http.get('/api/addons/vacay/available-users', () => HttpResponse.json({ users: [] }) ) ) } // ── Store seed helpers ──────────────────────────────────────────────────────── function seedVacay(overrides: Record = {}) { seedStore(useVacayStore, { users: [], pendingInvites: [], selectedUserId: 1, isFused: false, ...overrides, }) } function seedCurrentUser(id = 99) { seedStore(useAuthStore, { user: { id, username: `user${id}` } }) } // ───────────────────────────────────────────────────────────────────────────── beforeEach(() => { resetAllStores() }) describe('VacayPersons', () => { it('FE-COMP-VACAYPERSONS-001: Renders list of users', () => { seedVacay({ users: [{ id: 1, username: 'Alice', color: '#6366f1' }] }) seedCurrentUser(99) // different id so no "(you)" label render() expect(document.body).toHaveTextContent('Alice') }) it('FE-COMP-VACAYPERSONS-002: Current user shows "(you)" label', () => { seedVacay({ users: [{ id: 1, username: 'Alice', color: '#6366f1' }], selectedUserId: 1, }) seedCurrentUser(1) // Alice is the current user render() expect(document.body).toHaveTextContent('(you)') }) it('FE-COMP-VACAYPERSONS-003: Pending invite rendered with "(pending)" text', () => { seedVacay({ pendingInvites: [{ id: 10, user_id: 2, username: 'Bob' }], }) seedCurrentUser(1) render() expect(document.body).toHaveTextContent('Bob') expect(document.body).toHaveTextContent('(pending)') }) it('FE-COMP-VACAYPERSONS-004: Opens invite modal on UserPlus click', async () => { withNoAvailableUsers() const user = userEvent.setup() seedVacay() seedCurrentUser() render() // With no users seeded the first (and only) button is the UserPlus const [userPlusBtn] = screen.getAllByRole('button') await user.click(userPlusBtn) expect(screen.getByRole('heading', { name: 'Invite User' })).toBeInTheDocument() }) it('FE-COMP-VACAYPERSONS-005: Invite modal fetches and displays available users', async () => { withAvailableUsers() const user = userEvent.setup() seedVacay() seedCurrentUser() render() const [userPlusBtn] = screen.getAllByRole('button') await user.click(userPlusBtn) // Wait for MSW to respond and the CustomSelect trigger to appear await waitFor(() => { expect(screen.getByRole('button', { name: /select user/i })).toBeInTheDocument() }) // Open the CustomSelect dropdown await user.click(screen.getByRole('button', { name: /select user/i })) // Bob should appear as an option in the portal-rendered dropdown await waitFor(() => { expect(screen.getByText('Bob (bob@example.com)')).toBeInTheDocument() }) }) it('FE-COMP-VACAYPERSONS-006: Send invite button calls vacayStore.invite', async () => { withAvailableUsers() const inviteMock = vi.fn().mockResolvedValue(undefined) const user = userEvent.setup() seedVacay({ invite: inviteMock }) seedCurrentUser() render() // Open invite modal const [userPlusBtn] = screen.getAllByRole('button') await user.click(userPlusBtn) // Wait for CustomSelect to appear after MSW responds await waitFor(() => expect(screen.getByRole('button', { name: /select user/i })).toBeInTheDocument() ) // Open dropdown and select Bob await user.click(screen.getByRole('button', { name: /select user/i })) await waitFor(() => expect(screen.getByText('Bob (bob@example.com)')).toBeInTheDocument()) await user.click(screen.getByText('Bob (bob@example.com)')) // Send the invite await user.click(screen.getByRole('button', { name: /send invite/i })) expect(inviteMock).toHaveBeenCalledWith(2) }) it('FE-COMP-VACAYPERSONS-007: Invite modal closes on cancel', async () => { withNoAvailableUsers() const user = userEvent.setup() seedVacay() seedCurrentUser() render() const [userPlusBtn] = screen.getAllByRole('button') await user.click(userPlusBtn) expect(screen.getByRole('heading', { name: 'Invite User' })).toBeInTheDocument() // The Cancel button in the modal footer (no pending invites are seeded so it is unique) await user.click(screen.getByRole('button', { name: /^cancel$/i })) expect(screen.queryByRole('heading', { name: 'Invite User' })).not.toBeInTheDocument() }) it('FE-COMP-VACAYPERSONS-008: Color picker opens on color dot click', async () => { const user = userEvent.setup() seedVacay({ users: [{ id: 1, username: 'Alice', color: '#6366f1' }] }) seedCurrentUser(99) render() // The color dot button is identified by its title attribute "Change color" await user.click(screen.getByRole('button', { name: 'Change color' })) // Color picker modal heading is rendered via portal expect(screen.getByRole('heading', { name: 'Change color' })).toBeInTheDocument() }) it('FE-COMP-VACAYPERSONS-009: Selecting a preset color calls updateColor', async () => { const updateColorMock = vi.fn().mockResolvedValue(undefined) const user = userEvent.setup() seedVacay({ users: [{ id: 1, username: 'Alice', color: '#6366f1' }], updateColor: updateColorMock, }) seedCurrentUser(99) render() // Open color picker for Alice (id=1) await user.click(screen.getByRole('button', { name: 'Change color' })) await waitFor(() => expect(screen.getByRole('heading', { name: 'Change color' })).toBeInTheDocument() ) // Preset swatches: buttons with a backgroundColor inline style, no text content, no title. // The color dot trigger button is excluded because it has title="Change color". const allBtns = screen.getAllByRole('button') const colorSwatches = allBtns.filter( b => b.style.backgroundColor && !b.textContent?.trim() && !b.title ) expect(colorSwatches.length).toBeGreaterThan(0) // Click the first swatch – PRESET_COLORS[0] is '#6366f1' await user.click(colorSwatches[0]) expect(updateColorMock).toHaveBeenCalledWith('#6366f1', 1) }) it('FE-COMP-VACAYPERSONS-010: isFused enables row click to select user', async () => { const setSelectedUserIdMock = vi.fn() const user = userEvent.setup() seedVacay({ users: [ { id: 1, username: 'Alice', color: '#6366f1' }, { id: 2, username: 'Bob', color: '#ec4899' }, ], isFused: true, selectedUserId: 1, // non-null: prevents useEffect from calling the mock setSelectedUserId: setSelectedUserIdMock, }) seedCurrentUser(99) // distinct id to avoid the "(you)" label render() // Clicking Bob's name text bubbles up to the row div's onClick await user.click(screen.getByText('Bob')) expect(setSelectedUserIdMock).toHaveBeenCalledWith(2) }) it('FE-COMP-VACAYPERSONS-011: isFused false disables row selection', async () => { const setSelectedUserIdMock = vi.fn() const user = userEvent.setup() seedVacay({ users: [{ id: 2, username: 'Bob', color: '#ec4899' }], isFused: false, selectedUserId: 1, // non-null: prevents useEffect from calling the mock setSelectedUserId: setSelectedUserIdMock, }) seedCurrentUser(99) render() await user.click(screen.getByText('Bob')) expect(setSelectedUserIdMock).not.toHaveBeenCalled() }) })