mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
d4bb8be86b
Adds ~45 new and updated test files covering Admin, Collab, Dashboard, Map, Memories, PDF, Photos, Planner, Settings, Vacay, Weather components, pages, stores, and a WebSocket integration test.
269 lines
8.7 KiB
TypeScript
269 lines
8.7 KiB
TypeScript
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<string, unknown> = {}) {
|
||
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(<VacayPersons />)
|
||
|
||
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(<VacayPersons />)
|
||
|
||
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(<VacayPersons />)
|
||
|
||
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(<VacayPersons />)
|
||
|
||
// 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(<VacayPersons />)
|
||
|
||
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(<VacayPersons />)
|
||
|
||
// 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(<VacayPersons />)
|
||
|
||
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(<VacayPersons />)
|
||
|
||
// 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(<VacayPersons />)
|
||
|
||
// 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(<VacayPersons />)
|
||
|
||
// 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(<VacayPersons />)
|
||
|
||
await user.click(screen.getByText('Bob'))
|
||
|
||
expect(setSelectedUserIdMock).not.toHaveBeenCalled()
|
||
})
|
||
})
|