import CalendarPopover from '@/components/Calendar/CalendarModal'
import { getSecretLink } from '@/features/Calendars/CalendarApi'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import * as eventThunks from '@/features/Calendars/services'
import * as delegationThunks from '@/features/Calendars/services/updateDelegationCalendarAsync'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithProviders } from '../../utils/Renderwithproviders'
jest.mock('@/features/Calendars/CalendarApi', () => ({
getSecretLink: jest.fn()
}))
const mockThunkWithUnwrap = (resolvedValue: unknown = {}) =>
jest.fn().mockImplementation(() => {
const dispatchResult = Object.assign(Promise.resolve(resolvedValue), {
unwrap: () => Promise.resolve(resolvedValue)
})
return jest.fn().mockReturnValue(dispatchResult)
})
describe('CalendarPopover', () => {
const mockOnClose = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
jest
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
})
const renderPopover = (open = true) => {
const preloadedState = {
user: {
userData: {
sub: 'test',
email: 'test@test.com',
sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro',
openpaasId: '667037022b752d0026472254'
}
},
calendars: { list: {}, pending: true }
}
renderWithProviders(
,
preloadedState
)
}
it('renders popover and inputs', () => {
renderPopover()
expect(screen.getByLabelText(/Name/i)).toBeInTheDocument()
expect(screen.getByText('event.form.addDescription')).toBeInTheDocument()
})
it('updates name and description fields', () => {
renderPopover()
const nameInput = screen.getByLabelText(/Name/i)
fireEvent.change(nameInput, { target: { value: 'My Calendar' } })
expect(nameInput).toHaveValue('My Calendar')
fireEvent.click(screen.getByText('event.form.addDescription'))
const descInput = screen.getByLabelText(/Description/i)
fireEvent.change(descInput, { target: { value: 'Test description' } })
expect(descInput).toHaveValue('Test description')
})
it('dispatches createCalendar and calls onClose when Save clicked', async () => {
jest
.spyOn(eventThunks, 'createCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
renderPopover()
fireEvent.change(screen.getByLabelText(/Name/i), {
target: { value: 'Test Calendar' }
})
fireEvent.click(screen.getByText('event.form.addDescription'))
fireEvent.change(screen.getByLabelText(/Description/i), {
target: { value: 'Test Description' }
})
const colorButtons = screen.getAllByRole('button', {
name: /select color/i
})
fireEvent.click(colorButtons[0])
fireEvent.click(screen.getByRole('button', { name: /Create/i }))
await waitFor(() =>
expect(eventThunks.createCalendarAsync).toHaveBeenCalled()
)
await waitFor(() =>
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
)
})
it('calls onClose when Cancel clicked', () => {
renderPopover()
fireEvent.click(screen.getByRole('button', { name: /Cancel/i }))
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
})
})
describe('CalendarPopover (editing mode)', () => {
const mockOnClose = jest.fn()
const baseUser = {
userData: {
sub: 'test',
email: 'test@test.com',
sid: 'mockSid',
openpaasId: 'user1'
}
}
const existingCalendar: Calendar = {
id: 'user1/cal1',
link: '/calendars/user/cal1',
name: 'Work Calendar',
description: 'Team meetings',
color: { light: '#33B679' },
owner: { firstname: 'alice', emails: ['alice@example.com'] },
visibility: 'public',
events: {}
}
beforeEach(() => {
jest.clearAllMocks()
jest
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
})
it('prefills fields when calendar prop is given', () => {
renderWithProviders(
,
{ user: baseUser }
)
expect(screen.getByLabelText(/Name/i)).toHaveValue('Work Calendar')
expect(screen.getByLabelText(/Description/i)).toHaveValue('Team meetings')
})
test('Save button is disabled when name is empty or whitespace only', () => {
renderWithProviders(, {
user: baseUser
})
const saveButton = screen.getByRole('button', { name: /create/i })
expect(saveButton).toBeDisabled()
// only spaces
const nameInput = screen.getByLabelText(/name/i)
fireEvent.change(nameInput, { target: { value: ' ' } })
expect(saveButton).toBeDisabled()
// valid name
fireEvent.change(nameInput, { target: { value: 'Work Calendar' } })
expect(saveButton).toBeEnabled()
})
it('allows modifying and saving existing calendar', async () => {
jest
.spyOn(eventThunks, 'patchCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
renderWithProviders(
,
{ user: baseUser }
)
// Change name
fireEvent.change(screen.getByLabelText(/Name/i), {
target: { value: 'Updated Calendar' }
})
// Save
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
await waitFor(() =>
expect(eventThunks.patchCalendarAsync).toHaveBeenCalledWith(
expect.objectContaining({
calId: 'user1/cal1',
calLink: '/calendars/user/cal1',
patch: {
color: { light: '#33B679' },
desc: 'Team meetings',
name: 'Updated Calendar'
}
})
)
)
await waitFor(() => expect(mockOnClose).toHaveBeenCalled())
})
})
describe('CalendarPopover - Tabs Scenarios', () => {
const mockOnClose = jest.fn()
const baseUser = {
userData: {
openpaasId: 'user1'
}
}
const writeText = jest.fn()
Object.assign(navigator, {
clipboard: {
writeText
}
})
const existingCalendar: Calendar = {
id: 'user1/cal1',
link: '/calendars/user1/cal1.json',
name: 'Work Calendar',
description: 'Team meetings',
color: { light: '#33B679' },
owner: { firstname: 'alice', emails: ['alice@example.com'] },
visibility: 'public',
events: {}
}
beforeEach(() => {
jest.clearAllMocks()
jest
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
})
it('resets state after closing and reopening', () => {
const { rerender } = renderWithProviders(
,
{ user: baseUser }
)
// Enter some data
fireEvent.change(screen.getByLabelText(/Name/i), {
target: { value: 'Temp Calendar' }
})
fireEvent.click(screen.getByRole('button', { name: /Cancel/i }))
expect(mockOnClose).toHaveBeenCalled()
// Reopen: state should be reset
rerender()
expect(screen.getByLabelText(/Name/i)).toHaveValue('')
})
it('shows Access tab only when editing an existing calendar', () => {
renderWithProviders(
,
{ user: baseUser }
)
expect(screen.getByRole('tab', { name: /Access/i })).toBeInTheDocument()
})
it('does not show Access tab when creating new calendar', () => {
renderWithProviders(, {
user: baseUser
})
expect(
screen.queryByRole('tab', { name: /Access/i })
).not.toBeInTheDocument()
})
it('patches ACL when visibility changes', async () => {
jest
.spyOn(eventThunks, 'patchCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
jest
.spyOn(eventThunks, 'patchACLCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
renderWithProviders(
,
{ user: baseUser }
)
// By default: "All" (public) is selected
const publicButton = screen.getByRole('button', { name: /All/i })
const privateButton = screen.getByRole('button', { name: /You/i })
expect(publicButton).toHaveAttribute('aria-pressed', 'true')
expect(privateButton).toHaveAttribute('aria-pressed', 'false')
// Change to private
fireEvent.click(privateButton)
expect(privateButton).toHaveAttribute('aria-pressed', 'true')
expect(publicButton).toHaveAttribute('aria-pressed', 'false')
// Save
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
await waitFor(() =>
expect(eventThunks.patchACLCalendarAsync).toHaveBeenCalledWith(
expect.objectContaining({
calId: 'user1/cal1',
request: ''
})
)
)
})
it('copies CalDAV link from Access tab', async () => {
window.DAV_BASE_URL = 'https://cal.example.org'
Object.assign(navigator, {
clipboard: { writeText: jest.fn() }
})
;(getSecretLink as jest.Mock).mockResolvedValue({
secretLink: 'https://example.org/secret/initial'
})
renderWithProviders(
,
{ user: baseUser }
)
// Switch to Access tab
fireEvent.click(screen.getByRole('tab', { name: /Access/i }))
// Expect text field with caldav link
const input = screen.getByLabelText('calendar.caldav_access')
expect(input).toHaveValue('https://cal.example.org/calendars/user1/cal1')
// Click copy button (find button containing ContentCopyIcon)
const copyIcon = screen.getAllByTestId('ContentCopyIcon')[0]
const copyButton = copyIcon.closest('button')
if (copyButton) {
fireEvent.click(copyButton)
}
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'https://cal.example.org/calendars/user1/cal1'
)
// Snackbar should appear
await waitFor(() =>
expect(screen.getByText('common.link_copied')).toBeInTheDocument()
)
})
describe('Import flow', () => {
const file = new File(['test'], 'events.ics', { type: 'text/calendar' })
it("creates a new calendar and imports events when Import with 'new' target", async () => {
jest
.spyOn(eventThunks, 'createCalendarAsync')
.mockImplementation(mockThunkWithUnwrap())
jest
.spyOn(eventThunks, 'importEventFromFileAsync')
.mockImplementation(mockThunkWithUnwrap())
renderWithProviders(
,
{
user: baseUser
}
)
// Switch to Import tab
fireEvent.click(screen.getByRole('tab', { name: /Import/i }))
// Provide new calendar params
fireEvent.change(screen.getByLabelText(/Name/i), {
target: { value: 'Imported Calendar' }
})
const fileInput = screen.getByLabelText('common.select_file')
fireEvent.change(fileInput, { target: { files: [file] } })
// Click Import
fireEvent.click(screen.getByRole('button', { name: 'actions.import' }))
await waitFor(() =>
expect(eventThunks.createCalendarAsync).toHaveBeenCalled()
)
await waitFor(() =>
expect(eventThunks.importEventFromFileAsync).toHaveBeenCalled()
)
})
it('imports into an existing calendar when target is set', async () => {
jest
.spyOn(eventThunks, 'importEventFromFileAsync')
.mockImplementation(mockThunkWithUnwrap())
const calendars = {
'user1/cal1': existingCalendar
}
renderWithProviders(
,
{ user: baseUser, calendars: { list: calendars } }
)
fireEvent.click(screen.getByRole('tab', { name: /Import/i }))
const fileInput = screen.getByLabelText('common.select_file')
fireEvent.change(fileInput, { target: { files: [file] } })
fireEvent.click(screen.getByRole('button', { name: 'actions.import' }))
await waitFor(() =>
expect(eventThunks.importEventFromFileAsync).toHaveBeenCalledWith(
expect.objectContaining({
calLink: '/calendars/user1/cal1.json',
file
})
)
)
})
it('disables Import button until a file is uploaded', () => {
renderWithProviders(
,
{
user: baseUser
}
)
fireEvent.click(screen.getByRole('tab', { name: /Import/i }))
const importButton = screen.getByRole('button', {
name: 'actions.import'
})
expect(importButton).toBeDisabled()
})
})
it('fetches and resets the secret link', async () => {
window.DAV_BASE_URL = 'https://cal.example.org'
;(getSecretLink as jest.Mock)
.mockResolvedValueOnce({
secretLink: 'https://example.org/secret/initial'
})
.mockResolvedValueOnce({
secretLink: 'https://example.org/secret/new'
})
renderWithProviders(
,
{ user: baseUser }
)
fireEvent.click(screen.getByRole('tab', { name: /Access/i }))
await waitFor(() =>
expect(
screen.getByDisplayValue('https://example.org/secret/initial')
).toBeInTheDocument()
)
fireEvent.click(screen.getByRole('button', { name: /reset/i }))
await waitFor(() =>
expect(
screen.getByDisplayValue('https://example.org/secret/new')
).toBeInTheDocument()
)
expect(getSecretLink).toHaveBeenCalledWith(
existingCalendar.link.replace('.json', ''),
true
)
})
})