* #708 apply strictier linting rules and fix simple eslint bugs * #708 fix eslint errors relate to promise * #708 fix eslint import/no-extraneous-dependencies * #708 fix eslint errors of react-hook * #708 enable eslint check for typescript --------- Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
@@ -1,183 +1,181 @@
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { AttendanceValidation } from "@/features/Events/AttendanceValidation/AttendanceValidation";
|
||||
import { ContextualizedEvent } from "@/features/Events/EventsTypes";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { userData } from "@/features/User/userDataTypes";
|
||||
import { render } from "@testing-library/react";
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { AttendanceValidation } from '@/features/Events/AttendanceValidation/AttendanceValidation'
|
||||
import { ContextualizedEvent } from '@/features/Events/EventsTypes'
|
||||
import { userAttendee } from '@/features/User/models/attendee'
|
||||
import { userData } from '@/features/User/userDataTypes'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
jest.mock("twake-i18n", () => ({
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
jest.mock('twake-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key })
|
||||
}))
|
||||
|
||||
jest.mock("@/app/hooks", () => ({
|
||||
jest.mock('@/app/hooks', () => ({
|
||||
useAppDispatch: () => jest.fn(),
|
||||
useAppSelector: jest.fn(),
|
||||
}));
|
||||
useAppSelector: jest.fn()
|
||||
}))
|
||||
|
||||
const makeUser = (email: string): userData => ({
|
||||
email,
|
||||
given_name: "Alice",
|
||||
family_name: "User",
|
||||
name: "Alice User",
|
||||
sid: "user1",
|
||||
sub: "user1",
|
||||
});
|
||||
given_name: 'Alice',
|
||||
family_name: 'User',
|
||||
name: 'Alice User',
|
||||
sid: 'user1',
|
||||
sub: 'user1'
|
||||
})
|
||||
|
||||
const makeContext = (
|
||||
overrides: Partial<ContextualizedEvent> = {}
|
||||
): ContextualizedEvent =>
|
||||
({
|
||||
event: {
|
||||
uid: "event-1",
|
||||
calId: "user1/cal1",
|
||||
title: "Test",
|
||||
start: "2024-01-15T10:00:00",
|
||||
end: "2024-01-15T11:00:00",
|
||||
organizer: { cal_address: "owner@example.com" },
|
||||
attendee: [
|
||||
{ cal_address: "owner@example.com", partstat: "NEEDS-ACTION" },
|
||||
],
|
||||
uid: 'event-1',
|
||||
calId: 'user1/cal1',
|
||||
title: 'Test',
|
||||
start: '2024-01-15T10:00:00',
|
||||
end: '2024-01-15T11:00:00',
|
||||
organizer: { cal_address: 'owner@example.com' },
|
||||
attendee: [{ cal_address: 'owner@example.com', partstat: 'NEEDS-ACTION' }]
|
||||
},
|
||||
calendar: {
|
||||
id: "user1/cal1",
|
||||
name: "Test",
|
||||
id: 'user1/cal1',
|
||||
name: 'Test',
|
||||
delegated: false,
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
events: {},
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {}
|
||||
},
|
||||
currentUserAttendee: {
|
||||
cal_address: "alice@example.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
cal_address: 'alice@example.com',
|
||||
partstat: 'NEEDS-ACTION'
|
||||
},
|
||||
isOwn: true,
|
||||
isOrganizer: false,
|
||||
isRecurring: false,
|
||||
...overrides,
|
||||
}) as unknown as ContextualizedEvent;
|
||||
...overrides
|
||||
}) as unknown as ContextualizedEvent
|
||||
|
||||
const noopSetFunc = jest.fn();
|
||||
const noopSetFunc = jest.fn()
|
||||
|
||||
describe("AttendanceValidation - delegation", () => {
|
||||
describe("non-delegated calendar", () => {
|
||||
it("renders when isOwn and currentUserAttendee exists", () => {
|
||||
describe('AttendanceValidation - delegation', () => {
|
||||
describe('non-delegated calendar', () => {
|
||||
it('renders when isOwn and currentUserAttendee exists', () => {
|
||||
const { container } = render(
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={makeContext({ isOwn: true })}
|
||||
user={makeUser("alice@example.com")}
|
||||
user={makeUser('alice@example.com')}
|
||||
setAfterChoiceFunc={noopSetFunc}
|
||||
setOpenEditModePopup={noopSetFunc}
|
||||
/>
|
||||
);
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
});
|
||||
)
|
||||
expect(container.firstChild).not.toBeNull()
|
||||
})
|
||||
|
||||
it("returns null when not isOwn and not delegated", () => {
|
||||
it('returns null when not isOwn and not delegated', () => {
|
||||
const { container } = render(
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={makeContext({ isOwn: false })}
|
||||
user={makeUser("other@example.com")}
|
||||
user={makeUser('other@example.com')}
|
||||
setAfterChoiceFunc={noopSetFunc}
|
||||
setOpenEditModePopup={noopSetFunc}
|
||||
/>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("delegated calendar", () => {
|
||||
describe('delegated calendar', () => {
|
||||
const delegatedContext = makeContext({
|
||||
isOwn: false, // logged-in user is not the owner
|
||||
calendar: {
|
||||
id: "user2/cal1",
|
||||
name: "Delegated Cal",
|
||||
id: 'user2/cal1',
|
||||
name: 'Delegated Cal',
|
||||
delegated: true,
|
||||
owner: { emails: ["owner@example.com"] },
|
||||
events: {},
|
||||
owner: { emails: ['owner@example.com'] },
|
||||
events: {}
|
||||
} as Calendar,
|
||||
currentUserAttendee: {
|
||||
cal_address: "owner@example.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
cal_address: 'owner@example.com',
|
||||
partstat: 'NEEDS-ACTION'
|
||||
} as userAttendee,
|
||||
event: { class: "PUBLIC" },
|
||||
});
|
||||
event: { class: 'PUBLIC' }
|
||||
})
|
||||
|
||||
it("renders even when isOwn is false", () => {
|
||||
it('renders even when isOwn is false', () => {
|
||||
const { container } = render(
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={delegatedContext}
|
||||
user={makeUser("alice@example.com")}
|
||||
user={makeUser('alice@example.com')}
|
||||
setAfterChoiceFunc={noopSetFunc}
|
||||
setOpenEditModePopup={noopSetFunc}
|
||||
/>
|
||||
);
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
});
|
||||
)
|
||||
expect(container.firstChild).not.toBeNull()
|
||||
})
|
||||
|
||||
it("renders regardless of currentUserAttendee presence", () => {
|
||||
it('renders regardless of currentUserAttendee presence', () => {
|
||||
const { container } = render(
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={{
|
||||
...delegatedContext,
|
||||
currentUserAttendee: undefined,
|
||||
currentUserAttendee: undefined
|
||||
}}
|
||||
user={makeUser("alice@example.com")}
|
||||
user={makeUser('alice@example.com')}
|
||||
setAfterChoiceFunc={noopSetFunc}
|
||||
setOpenEditModePopup={noopSetFunc}
|
||||
/>
|
||||
);
|
||||
)
|
||||
// delegated flag alone should keep it visible
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
});
|
||||
});
|
||||
expect(container.firstChild).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("resource calendar", () => {
|
||||
describe('resource calendar', () => {
|
||||
const resourceContext = makeContext({
|
||||
isOwn: false,
|
||||
calendar: {
|
||||
id: "res1/cal1",
|
||||
name: "Resource Cal",
|
||||
id: 'res1/cal1',
|
||||
name: 'Resource Cal',
|
||||
delegated: false,
|
||||
owner: {
|
||||
emails: ["resource@example.com"],
|
||||
emails: ['resource@example.com'],
|
||||
resource: true,
|
||||
administrators: [{ id: "admin1" }],
|
||||
administrators: [{ id: 'admin1' }]
|
||||
},
|
||||
events: {},
|
||||
} as Calendar,
|
||||
});
|
||||
events: {}
|
||||
} as Calendar
|
||||
})
|
||||
|
||||
it("renders when user is an administrator of the resource", () => {
|
||||
it('renders when user is an administrator of the resource', () => {
|
||||
const { getByText } = render(
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={resourceContext}
|
||||
user={
|
||||
{
|
||||
...makeUser("admin@example.com"),
|
||||
openpaasId: "admin1",
|
||||
...makeUser('admin@example.com'),
|
||||
openpaasId: 'admin1'
|
||||
} as userData
|
||||
}
|
||||
setAfterChoiceFunc={noopSetFunc}
|
||||
setOpenEditModePopup={noopSetFunc}
|
||||
/>
|
||||
);
|
||||
expect(getByText("eventPreview.authorizeQuestion")).toBeTruthy();
|
||||
});
|
||||
)
|
||||
expect(getByText('eventPreview.authorizeQuestion')).toBeTruthy()
|
||||
})
|
||||
|
||||
it("returns null when user is not an administrator of the resource", () => {
|
||||
it('returns null when user is not an administrator of the resource', () => {
|
||||
const { container } = render(
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={resourceContext}
|
||||
user={
|
||||
{
|
||||
...makeUser("other@example.com"),
|
||||
openpaasId: "other1",
|
||||
...makeUser('other@example.com'),
|
||||
openpaasId: 'other1'
|
||||
} as userData
|
||||
}
|
||||
setAfterChoiceFunc={noopSetFunc}
|
||||
setOpenEditModePopup={noopSetFunc}
|
||||
/>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,131 +1,131 @@
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import EventUpdateModal from '@/features/Events/EventUpdateModal'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
describe("Event Full Display — delegated calendar move", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('Event Full Display — delegated calendar move', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
||||
const testDate = new Date('2025-01-15T10:00:00.000Z')
|
||||
const testEndDate = new Date('2025-01-15T11:00:00.000Z')
|
||||
|
||||
const baseUserState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const preloadedDelegatedCals = {
|
||||
...baseUserState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
id: "user1/cal1",
|
||||
name: "Calendar One",
|
||||
color: "#FF0000",
|
||||
'user1/cal1': {
|
||||
id: 'user1/cal1',
|
||||
name: 'Calendar One',
|
||||
color: '#FF0000',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "user1/cal1",
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
calId: 'user1/cal1',
|
||||
start: testDate.toISOString(),
|
||||
end: testEndDate.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
URL: "/calendar/user1/cal1/event1.ics",
|
||||
},
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }],
|
||||
URL: '/calendar/user1/cal1/event1.ics'
|
||||
}
|
||||
},
|
||||
link: "/calendar/user1/cal1.json",
|
||||
link: '/calendar/user1/cal1.json'
|
||||
},
|
||||
"user2/cal2": {
|
||||
id: "user2/cal2",
|
||||
name: "Delegated Calendar",
|
||||
color: "#00FF00",
|
||||
'user2/cal2': {
|
||||
id: 'user2/cal2',
|
||||
name: 'Delegated Calendar',
|
||||
color: '#00FF00',
|
||||
delegated: true,
|
||||
access: { write: true },
|
||||
owner: {
|
||||
emails: ["delegate@test.com"],
|
||||
name: "Delegate User",
|
||||
emails: ['delegate@test.com'],
|
||||
name: 'Delegate User'
|
||||
},
|
||||
events: {},
|
||||
link: "/calendar/user2/cal2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
link: '/calendar/user2/cal2.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it("calls deleteEventAsync and putEventAsync — and NOT moveEventAsync — when moving to a delegated calendar", async () => {
|
||||
it('calls deleteEventAsync and putEventAsync — and NOT moveEventAsync — when moving to a delegated calendar', async () => {
|
||||
const spyPut = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
const spyDelete = jest
|
||||
.spyOn(eventThunks, "deleteEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'deleteEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
const spyMove = jest
|
||||
.spyOn(eventThunks, "moveEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'moveEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"user1/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'user1/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
preloadedDelegatedCals
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
|
||||
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||
const option = await screen.findByText("Delegated Calendar");
|
||||
fireEvent.click(option);
|
||||
const calendarSelect = screen.getByLabelText('event.form.calendar')
|
||||
await act(async () => fireEvent.mouseDown(calendarSelect))
|
||||
const option = await screen.findByText('Delegated Calendar')
|
||||
fireEvent.click(option)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
})
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyPut).toHaveBeenCalled();
|
||||
expect(spyPut).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyDelete).toHaveBeenCalled();
|
||||
expect(spyDelete).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
expect(spyMove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(spyMove).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,422 +1,422 @@
|
||||
import { InfoRow } from "@/components/Event/InfoRow";
|
||||
import { LONG_DATE_FORMAT } from "@/components/Event/utils/dateTimeFormatters";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import dayjs from "dayjs";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import { InfoRow } from '@/components/Event/InfoRow'
|
||||
import { LONG_DATE_FORMAT } from '@/components/Event/utils/dateTimeFormatters'
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import EventUpdateModal from '@/features/Events/EventUpdateModal'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import dayjs from 'dayjs'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
describe("Event Full Display", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
const day = new Date("2025-01-15T10:00:00.000Z"); // Fixed date in UTC: Jan 15, 2025 10:00 AM UTC
|
||||
describe('Event Full Display', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
const day = new Date('2025-01-15T10:00:00.000Z') // Fixed date in UTC: Jan 15, 2025 10:00 AM UTC
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "First Calendar",
|
||||
color: "#FF0000",
|
||||
'667037022b752d0026472254/cal1': {
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
name: 'First Calendar',
|
||||
color: '#FF0000',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: day.toISOString(),
|
||||
end: day.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [
|
||||
{
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com',
|
||||
partstat: 'NEEDS-ACTION',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL'
|
||||
},
|
||||
{
|
||||
cn: "John",
|
||||
cal_address: "john@test.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
cn: 'John',
|
||||
cal_address: 'john@test.com',
|
||||
partstat: 'NEEDS-ACTION',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"otherCal/cal": {
|
||||
id: "otherCal/cal",
|
||||
name: "Calendar 1",
|
||||
color: "#FF0000",
|
||||
'otherCal/cal': {
|
||||
id: 'otherCal/cal',
|
||||
name: 'Calendar 1',
|
||||
color: '#FF0000',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
calId: "otherCal/cal",
|
||||
title: "Test Event Other cal",
|
||||
uid: 'event1',
|
||||
calId: 'otherCal/cal',
|
||||
title: 'Test Event Other cal',
|
||||
start: day.toISOString(),
|
||||
end: day.toISOString(),
|
||||
organizer: { cn: "john", cal_address: "john@test.com" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
organizer: { cn: 'john', cal_address: 'john@test.com' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it("renders correctly event data with fixed timezone", () => {
|
||||
it('renders correctly event data with fixed timezone', () => {
|
||||
// Use fixed timezone UTC for consistent test results across all environments
|
||||
const fixedDate = new Date("2025-01-15T10:00:00.000Z"); // 10AM UTC
|
||||
const endDate = new Date(fixedDate.getTime() + 3600000); // 11AM UTC
|
||||
const fixedDate = new Date('2025-01-15T10:00:00.000Z') // 10AM UTC
|
||||
const endDate = new Date(fixedDate.getTime() + 3600000) // 11AM UTC
|
||||
|
||||
// With UTC timezone set, formatLocalDateTime produces predictable values
|
||||
const expectedStart = "2025-01-15T10:00"; // 10:00 AM UTC
|
||||
const expectedEnd = "2025-01-15T11:00"; // 11:00 AM UTC
|
||||
const expectedStart = '2025-01-15T10:00' // 10:00 AM UTC
|
||||
const expectedEnd = '2025-01-15T11:00' // 11:00 AM UTC
|
||||
|
||||
const stateWithFixedDate = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
'667037022b752d0026472254/cal1': {
|
||||
...preloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
'667037022b752d0026472254/cal1'
|
||||
].events.event1,
|
||||
start: fixedDate.toISOString(),
|
||||
end: endDate.toISOString(),
|
||||
timezone: "UTC",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
timezone: 'UTC'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
stateWithFixedDate
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByDisplayValue("Test Event")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('Test Event')).toBeInTheDocument()
|
||||
|
||||
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const endTimeInput = screen.getByTestId("end-time-input");
|
||||
const startDateInput = screen.getByTestId('start-date-input')
|
||||
const startTimeInput = screen.getByTestId('start-time-input')
|
||||
const endTimeInput = screen.getByTestId('end-time-input')
|
||||
|
||||
expect(startDateInput).toBeInTheDocument();
|
||||
expect(startTimeInput).toBeInTheDocument();
|
||||
expect(endTimeInput).toBeInTheDocument();
|
||||
expect(startDateInput).toBeInTheDocument()
|
||||
expect(startTimeInput).toBeInTheDocument()
|
||||
expect(endTimeInput).toBeInTheDocument()
|
||||
|
||||
expect(startDateInput).toHaveValue(
|
||||
dayjs(expectedStart).format(LONG_DATE_FORMAT)
|
||||
);
|
||||
expect(startTimeInput).toHaveValue(dayjs(expectedStart).format("HH:mm"));
|
||||
)
|
||||
expect(startTimeInput).toHaveValue(dayjs(expectedStart).format('HH:mm'))
|
||||
|
||||
expect(endTimeInput).toHaveValue(dayjs(expectedEnd).format("HH:mm"));
|
||||
expect(endTimeInput).toHaveValue(dayjs(expectedEnd).format('HH:mm'))
|
||||
|
||||
expect(screen.getByText("First Calendar")).toBeInTheDocument();
|
||||
});
|
||||
it("calls onClose when Cancel clicked", () => {
|
||||
expect(screen.getByText('First Calendar')).toBeInTheDocument()
|
||||
})
|
||||
it('calls onClose when Cancel clicked', () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
fireEvent.click(screen.getAllByTestId("CloseIcon")[0]);
|
||||
)
|
||||
fireEvent.click(screen.getAllByTestId('CloseIcon')[0])
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
it("toggle Show More reveals extra fields", async () => {
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
|
||||
})
|
||||
it('toggle Show More reveals extra fields', async () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
act(() => {
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
});
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("event.form.notification")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('event.form.notification')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// EventDisplay modal doesn't have Repeat checkbox, only RepeatEvent component
|
||||
// which shows repetition settings when repetition data exists
|
||||
// Since test event has no repetition data, RepeatEvent component won't show Repeat checkbox
|
||||
fireEvent.click(screen.getByRole("button", { name: /Show Less/i }));
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /Show Less/i }))
|
||||
})
|
||||
|
||||
it("can edit title when user is organizer", () => {
|
||||
it('can edit title when user is organizer', () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
const titleField = screen.getByLabelText("event.form.title");
|
||||
fireEvent.change(titleField, { target: { value: "New Title" } });
|
||||
expect(screen.getByDisplayValue("New Title")).toBeInTheDocument();
|
||||
});
|
||||
it("toggle all-day updates end date correctly", () => {
|
||||
)
|
||||
const titleField = screen.getByLabelText('event.form.title')
|
||||
fireEvent.change(titleField, { target: { value: 'New Title' } })
|
||||
expect(screen.getByDisplayValue('New Title')).toBeInTheDocument()
|
||||
})
|
||||
it('toggle all-day updates end date correctly', () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
fireEvent.click(allDayCheckbox);
|
||||
expect(allDayCheckbox).toBeChecked();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
const allDayCheckbox = screen.getByLabelText('event.form.allDay')
|
||||
fireEvent.click(allDayCheckbox)
|
||||
expect(allDayCheckbox).toBeChecked()
|
||||
|
||||
const expectedDate = dayjs(day).format(LONG_DATE_FORMAT);
|
||||
const expectedDate = dayjs(day).format(LONG_DATE_FORMAT)
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const endDateInput = screen.getByTestId("end-date-input");
|
||||
const startDateInput = screen.getByTestId('start-date-input')
|
||||
const endDateInput = screen.getByTestId('end-date-input')
|
||||
|
||||
expect(startDateInput).toHaveValue(expectedDate);
|
||||
expect(endDateInput).toHaveValue(expectedDate);
|
||||
});
|
||||
expect(startDateInput).toHaveValue(expectedDate)
|
||||
expect(endDateInput).toHaveValue(expectedDate)
|
||||
})
|
||||
|
||||
it("saves event and moves it when calendar is changed", async () => {
|
||||
it('saves event and moves it when calendar is changed', async () => {
|
||||
const spyPut = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
const spyMove = jest
|
||||
.spyOn(eventThunks, "moveEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'moveEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
|
||||
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
||||
const testDate = new Date('2025-01-15T10:00:00.000Z')
|
||||
const testEndDate = new Date('2025-01-15T11:00:00.000Z')
|
||||
const preloadedTwoCals = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Calendar One",
|
||||
color: "#FF0000",
|
||||
'667037022b752d0026472254/cal1': {
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
name: 'Calendar One',
|
||||
color: '#FF0000',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: testDate.toISOString(),
|
||||
end: testEndDate.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
"667037022b752d0026472254/cal2": {
|
||||
id: "667037022b752d0026472254/cal2",
|
||||
name: "Calendar Two",
|
||||
color: "#00FF00",
|
||||
events: {},
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
'667037022b752d0026472254/cal2': {
|
||||
id: '667037022b752d0026472254/cal2',
|
||||
name: 'Calendar Two',
|
||||
color: '#00FF00',
|
||||
events: {}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
preloadedTwoCals
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
const calendarSelect = screen.getByLabelText('event.form.calendar')
|
||||
await act(async () => fireEvent.mouseDown(calendarSelect))
|
||||
|
||||
const option = await screen.findByText("Calendar Two");
|
||||
fireEvent.click(option);
|
||||
const option = await screen.findByText('Calendar Two')
|
||||
fireEvent.click(option)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
})
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyPut).toHaveBeenCalled();
|
||||
expect(spyPut).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyMove).toHaveBeenCalled();
|
||||
expect(spyMove).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("edit modal displays event time in original event timezone", () => {
|
||||
it('edit modal displays event time in original event timezone', () => {
|
||||
// GIVEN user timezone is UTC+2
|
||||
// WHEN the user edits an event at 2PM UTC+7 (Asia/Bangkok)
|
||||
// THEN the update modal displays the time as 2PM in Asia/Bangkok timezone
|
||||
const eventDateUTC7 = new Date("2025-01-15T07:00:00.000Z"); // 7AM UTC = 2PM UTC+7
|
||||
const eventDateUTC7 = new Date('2025-01-15T07:00:00.000Z') // 7AM UTC = 2PM UTC+7
|
||||
|
||||
const stateWithTimezone = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Test Calendar",
|
||||
color: "#FF0000",
|
||||
'667037022b752d0026472254/cal1': {
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
name: 'Test Calendar',
|
||||
color: '#FF0000',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Timezone Edit Test",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: 'event1',
|
||||
title: 'Timezone Edit Test',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: eventDateUTC7.toISOString(),
|
||||
end: new Date(eventDateUTC7.getTime() + 3600000).toISOString(),
|
||||
timezone: "Asia/Bangkok",
|
||||
timezone: 'Asia/Bangkok',
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
stateWithTimezone
|
||||
);
|
||||
)
|
||||
|
||||
// Expand to show timezone selector (normal mode hides it)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
// The timezone select should have Asia/Bangkok selected
|
||||
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i);
|
||||
expect(timeZone).toBeInTheDocument();
|
||||
});
|
||||
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i)
|
||||
expect(timeZone).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("InfoRow renders error style when error prop is true", () => {
|
||||
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
|
||||
expect(screen.getByText("Bad")).toBeInTheDocument();
|
||||
});
|
||||
it('InfoRow renders error style when error prop is true', () => {
|
||||
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />)
|
||||
expect(screen.getByText('Bad')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("can remove an attendee with the close button", () => {
|
||||
it('can remove an attendee with the close button', () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
const removeBtn = screen.getAllByTestId("CloseIcon").pop()!;
|
||||
fireEvent.click(removeBtn);
|
||||
const removeBtn = screen.getAllByTestId('CloseIcon').pop()!
|
||||
fireEvent.click(removeBtn)
|
||||
|
||||
expect(screen.queryByText(/John/)).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText(/John/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders video conference info when x_openpass_videoconference exists", () => {
|
||||
it('renders video conference info when x_openpass_videoconference exists', () => {
|
||||
const videoState = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
'667037022b752d0026472254/cal1': {
|
||||
...preloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
'667037022b752d0026472254/cal1'
|
||||
].events.event1,
|
||||
x_openpass_videoconference: "https://meet.test/video",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
x_openpass_videoconference: 'https://meet.test/video'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'event1'}
|
||||
/>,
|
||||
videoState
|
||||
);
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText("event.form.joinVisioConference")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
screen.getByText('event.form.joinVisioConference')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import * as calendarsApi from "@/features/Calendars/CalendarApi";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventPopover from "@/features/Events/EventModal";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { DateSelectArg } from "@fullcalendar/core";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import * as calendarsApi from '@/features/Calendars/CalendarApi'
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import EventPopover from '@/features/Events/EventModal'
|
||||
import { api } from '@/utils/apiUtils'
|
||||
import { DateSelectArg } from '@fullcalendar/core'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/utils/apiUtils");
|
||||
jest.mock('@/utils/apiUtils')
|
||||
|
||||
describe("EventPopover", () => {
|
||||
describe('EventPopover', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
const mockSetSelectedRange = jest.fn();
|
||||
const mockCalendarRef = { current: { select: jest.fn() } } as any;
|
||||
const mockOnClose = jest.fn()
|
||||
const mockSetSelectedRange = jest.fn()
|
||||
const mockCalendarRef = { current: { select: jest.fn() } } as any
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Calendar 1",
|
||||
color: "#FF0000",
|
||||
},
|
||||
"667037022b752d0026472254/cal2": {
|
||||
id: "667037022b752d0026472254/cal2",
|
||||
name: "Calendar 2",
|
||||
color: "#00FF00",
|
||||
'667037022b752d0026472254/cal1': {
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
name: 'Calendar 1',
|
||||
color: '#FF0000'
|
||||
},
|
||||
'667037022b752d0026472254/cal2': {
|
||||
id: '667037022b752d0026472254/cal2',
|
||||
name: 'Calendar 2',
|
||||
color: '#00FF00'
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
const mockUsers = [
|
||||
{
|
||||
id: "john@example.com",
|
||||
objectType: "user",
|
||||
id: 'john@example.com',
|
||||
objectType: 'user',
|
||||
emailAddresses: [
|
||||
{
|
||||
value: "john@example.com",
|
||||
type: "default",
|
||||
},
|
||||
value: 'john@example.com',
|
||||
type: 'default'
|
||||
}
|
||||
],
|
||||
names: [
|
||||
{
|
||||
displayName: "John Doe",
|
||||
type: "default",
|
||||
},
|
||||
],
|
||||
displayName: 'John Doe',
|
||||
type: 'default'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "jane@example.com",
|
||||
objectType: "user",
|
||||
id: 'jane@example.com',
|
||||
objectType: 'user',
|
||||
emailAddresses: [
|
||||
{
|
||||
value: "jane@example.com",
|
||||
type: "default",
|
||||
},
|
||||
value: 'jane@example.com',
|
||||
type: 'default'
|
||||
}
|
||||
],
|
||||
names: [
|
||||
{
|
||||
displayName: "Jane Smith",
|
||||
type: "default",
|
||||
},
|
||||
],
|
||||
displayName: 'Jane Smith',
|
||||
type: 'default'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "room1@example.com",
|
||||
objectType: "resource",
|
||||
id: 'room1@example.com',
|
||||
objectType: 'resource',
|
||||
emailAddresses: [
|
||||
{
|
||||
value: "room1@example.com",
|
||||
type: "default",
|
||||
},
|
||||
value: 'room1@example.com',
|
||||
type: 'default'
|
||||
}
|
||||
],
|
||||
names: [
|
||||
{
|
||||
displayName: "Room 1",
|
||||
type: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
(api.post as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockUsers),
|
||||
});
|
||||
displayName: 'Room 1',
|
||||
type: 'default'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
;(api.post as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockUsers)
|
||||
})
|
||||
|
||||
const defaultSelectedRange = {
|
||||
startStr: "2025-07-18T09:00",
|
||||
endStr: "2025-07-18T10:00",
|
||||
start: new Date("2025-07-18T09:00"),
|
||||
end: new Date("2025-07-18T10:00"),
|
||||
startStr: '2025-07-18T09:00',
|
||||
endStr: '2025-07-18T10:00',
|
||||
start: new Date('2025-07-18T09:00'),
|
||||
end: new Date('2025-07-18T10:00'),
|
||||
allDay: false,
|
||||
resource: undefined,
|
||||
} as unknown as DateSelectArg;
|
||||
resource: undefined
|
||||
} as unknown as DateSelectArg
|
||||
|
||||
const renderPopover = (selectedRange = defaultSelectedRange) => {
|
||||
renderWithProviders(
|
||||
@@ -122,461 +122,459 @@ describe("EventPopover", () => {
|
||||
calendarRef={mockCalendarRef}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
it("renders correctly with inputs and calendar options", () => {
|
||||
renderPopover();
|
||||
it('renders correctly with inputs and calendar options', () => {
|
||||
renderPopover()
|
||||
|
||||
// Check dialog title
|
||||
expect(screen.getByText("event.createEvent")).toBeInTheDocument();
|
||||
expect(screen.getByText('event.createEvent')).toBeInTheDocument()
|
||||
|
||||
// Check inputs exist by their roles
|
||||
const titleInput = screen.getByRole("textbox", { name: /title/i });
|
||||
expect(titleInput).toBeInTheDocument();
|
||||
const titleInput = screen.getByRole('textbox', { name: /title/i })
|
||||
expect(titleInput).toBeInTheDocument()
|
||||
|
||||
// Description input is only visible after clicking "Add description" button
|
||||
const addDescriptionButton = screen.getByRole("button", {
|
||||
name: "event.form.addDescription",
|
||||
});
|
||||
expect(addDescriptionButton).toBeInTheDocument();
|
||||
const addDescriptionButton = screen.getByRole('button', {
|
||||
name: 'event.form.addDescription'
|
||||
})
|
||||
expect(addDescriptionButton).toBeInTheDocument()
|
||||
|
||||
// In normal mode calendar is a SectionPreviewRow (button), not combobox
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Calendar 1" })
|
||||
).toBeInTheDocument();
|
||||
screen.getByRole('button', { name: 'Calendar 1' })
|
||||
).toBeInTheDocument()
|
||||
|
||||
// Check button
|
||||
expect(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
).toBeInTheDocument();
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
).toBeInTheDocument()
|
||||
|
||||
// Extended mode
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
|
||||
// Back button appears
|
||||
expect(screen.getByLabelText("show less")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('show less')).toBeInTheDocument()
|
||||
|
||||
// Extended labels appear
|
||||
expect(screen.getAllByText("event.form.repeat")).toHaveLength(1);
|
||||
expect(screen.getAllByText("event.form.notification")).toHaveLength(1);
|
||||
expect(screen.getAllByText("event.form.visibleTo")).toHaveLength(1);
|
||||
expect(screen.getAllByText("event.form.showMeAs")).toHaveLength(1);
|
||||
});
|
||||
expect(screen.getAllByText('event.form.repeat')).toHaveLength(1)
|
||||
expect(screen.getAllByText('event.form.notification')).toHaveLength(1)
|
||||
expect(screen.getAllByText('event.form.visibleTo')).toHaveLength(1)
|
||||
expect(screen.getAllByText('event.form.showMeAs')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("fills start from selectedRange", () => {
|
||||
it('fills start from selectedRange', () => {
|
||||
renderPopover({
|
||||
startStr: "2026-07-20T10:00",
|
||||
endStr: "2026-07-20T12:00",
|
||||
start: new Date("2026-07-20T10:00"),
|
||||
end: new Date("2026-07-20T12:00"),
|
||||
allDay: false,
|
||||
} as unknown as DateSelectArg);
|
||||
startStr: '2026-07-20T10:00',
|
||||
endStr: '2026-07-20T12:00',
|
||||
start: new Date('2026-07-20T10:00'),
|
||||
end: new Date('2026-07-20T12:00'),
|
||||
allDay: false
|
||||
} as unknown as DateSelectArg)
|
||||
|
||||
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
expect(screen.getByTestId("start-date-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("start-time-input")).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
expect(screen.getByTestId('start-date-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('start-time-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("updates inputs on change", () => {
|
||||
renderPopover();
|
||||
it('updates inputs on change', () => {
|
||||
renderPopover()
|
||||
|
||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||
target: { value: "My Event" },
|
||||
});
|
||||
expect(screen.getByLabelText("event.form.title")).toHaveValue("My Event");
|
||||
fireEvent.change(screen.getByLabelText('event.form.title'), {
|
||||
target: { value: 'My Event' }
|
||||
})
|
||||
expect(screen.getByLabelText('event.form.title')).toHaveValue('My Event')
|
||||
|
||||
// Click "Add description" button first
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "event.form.addDescription" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'event.form.addDescription' })
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByLabelText("event.form.description"), {
|
||||
target: { value: "Event Description" },
|
||||
});
|
||||
expect(screen.getByLabelText("event.form.description")).toHaveValue(
|
||||
"Event Description"
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('event.form.description'), {
|
||||
target: { value: 'Event Description' }
|
||||
})
|
||||
expect(screen.getByLabelText('event.form.description')).toHaveValue(
|
||||
'Event Description'
|
||||
)
|
||||
|
||||
// Expand location section (normal mode shows SectionPreviewRow)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "event.form.locationPlaceholder" })
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("event.form.location"), {
|
||||
target: { value: "Conference Room" },
|
||||
});
|
||||
expect(screen.getByLabelText("event.form.location")).toHaveValue(
|
||||
"Conference Room"
|
||||
);
|
||||
});
|
||||
screen.getByRole('button', { name: 'event.form.locationPlaceholder' })
|
||||
)
|
||||
fireEvent.change(screen.getByLabelText('event.form.location'), {
|
||||
target: { value: 'Conference Room' }
|
||||
})
|
||||
expect(screen.getByLabelText('event.form.location')).toHaveValue(
|
||||
'Conference Room'
|
||||
)
|
||||
})
|
||||
|
||||
it("changes selected calendar", async () => {
|
||||
renderPopover();
|
||||
it('changes selected calendar', async () => {
|
||||
renderPopover()
|
||||
|
||||
// Expand to show calendar combobox (normal mode shows SectionPreviewRow)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
const select = screen.getByLabelText("event.form.calendar");
|
||||
fireEvent.mouseDown(select); // Open menu
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
const select = screen.getByLabelText('event.form.calendar')
|
||||
fireEvent.mouseDown(select) // Open menu
|
||||
|
||||
const option = await screen.findByText("Calendar 2");
|
||||
fireEvent.click(option);
|
||||
const option = await screen.findByText('Calendar 2')
|
||||
fireEvent.click(option)
|
||||
|
||||
const calendarSelect = screen.getByRole("combobox", { name: /Calendar/i });
|
||||
expect(calendarSelect).toHaveTextContent("Calendar 2");
|
||||
});
|
||||
it("adds a attendee", async () => {
|
||||
jest.useFakeTimers();
|
||||
const calendarSelect = screen.getByRole('combobox', { name: /Calendar/i })
|
||||
expect(calendarSelect).toHaveTextContent('Calendar 2')
|
||||
})
|
||||
it('adds a attendee', async () => {
|
||||
jest.useFakeTimers()
|
||||
try {
|
||||
jest
|
||||
.spyOn(calendarsApi, "getCalendars")
|
||||
.mockReturnValue({ json: jest.fn() });
|
||||
renderPopover();
|
||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||
target: { value: "newEvent" },
|
||||
});
|
||||
const select = screen.getByLabelText("peopleSearch.label");
|
||||
.spyOn(calendarsApi, 'getCalendars')
|
||||
.mockReturnValue({ json: jest.fn() })
|
||||
renderPopover()
|
||||
fireEvent.change(screen.getByLabelText('event.form.title'), {
|
||||
target: { value: 'newEvent' }
|
||||
})
|
||||
const select = screen.getByLabelText('peopleSearch.label')
|
||||
|
||||
act(() => {
|
||||
select.focus();
|
||||
fireEvent.mouseDown(select);
|
||||
userEvent.type(select, "john");
|
||||
});
|
||||
select.focus()
|
||||
fireEvent.mouseDown(select)
|
||||
userEvent.type(select, 'john')
|
||||
})
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
await waitFor(() => expect(api.post).toHaveBeenCalledTimes(1));
|
||||
jest.advanceTimersByTime(400)
|
||||
})
|
||||
await waitFor(() => expect(api.post).toHaveBeenCalledTimes(1))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
||||
})
|
||||
await act(async () => {
|
||||
userEvent.click(screen.getByText("John Doe"));
|
||||
});
|
||||
userEvent.click(screen.getByText('John Doe'))
|
||||
})
|
||||
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const receivedPayload = spy.mock.calls[0][0];
|
||||
const receivedPayload = spy.mock.calls[0][0]
|
||||
|
||||
expect(receivedPayload.cal).toEqual(
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
||||
);
|
||||
preloadedState.calendars.list['667037022b752d0026472254/cal1']
|
||||
)
|
||||
|
||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2);
|
||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2)
|
||||
expect(receivedPayload.newEvent.attendee).toStrictEqual([
|
||||
{
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
role: "CHAIR",
|
||||
cutype: "INDIVIDUAL",
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'FALSE',
|
||||
role: 'CHAIR',
|
||||
cutype: 'INDIVIDUAL'
|
||||
},
|
||||
{
|
||||
cn: "John Doe",
|
||||
cal_address: "john@example.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "FALSE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
]);
|
||||
cn: 'John Doe',
|
||||
cal_address: 'john@example.com',
|
||||
partstat: 'NEEDS-ACTION',
|
||||
rsvp: 'FALSE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL'
|
||||
}
|
||||
])
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
jest.useRealTimers()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it("adds a resource", async () => {
|
||||
jest.useFakeTimers();
|
||||
it('adds a resource', async () => {
|
||||
jest.useFakeTimers()
|
||||
try {
|
||||
renderPopover();
|
||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||
target: { value: "newEventWithResource" },
|
||||
});
|
||||
renderPopover()
|
||||
fireEvent.change(screen.getByLabelText('event.form.title'), {
|
||||
target: { value: 'newEventWithResource' }
|
||||
})
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
|
||||
const resourceCombobox = screen.getByPlaceholderText(
|
||||
"resourceSearch.placeholder"
|
||||
);
|
||||
'resourceSearch.placeholder'
|
||||
)
|
||||
|
||||
act(() => {
|
||||
resourceCombobox.focus();
|
||||
fireEvent.mouseDown(resourceCombobox);
|
||||
});
|
||||
await userEvent.type(resourceCombobox, "room");
|
||||
resourceCombobox.focus()
|
||||
fireEvent.mouseDown(resourceCombobox)
|
||||
})
|
||||
await userEvent.type(resourceCombobox, 'room')
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
jest.advanceTimersByTime(400)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Room 1")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Room 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByText("Room 1"));
|
||||
await userEvent.click(screen.getByText('Room 1'))
|
||||
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const receivedPayload = spy.mock.calls[0][0];
|
||||
const receivedPayload = spy.mock.calls[0][0]
|
||||
|
||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2); // Organizer + 1 resource
|
||||
expect(receivedPayload.newEvent.attendee).toHaveLength(2) // Organizer + 1 resource
|
||||
expect(receivedPayload.newEvent.attendee[1]).toStrictEqual({
|
||||
cn: "Room 1",
|
||||
cal_address: "room1@example.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "RESOURCE",
|
||||
});
|
||||
cn: 'Room 1',
|
||||
cal_address: 'room1@example.com',
|
||||
partstat: 'NEEDS-ACTION',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'RESOURCE'
|
||||
})
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
jest.useRealTimers()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it("dispatches putEventAsync and calls onClose when Save is clicked", async () => {
|
||||
renderPopover();
|
||||
it('dispatches putEventAsync and calls onClose when Save is clicked', async () => {
|
||||
renderPopover()
|
||||
const newEvent = {
|
||||
title: "Meeting",
|
||||
title: 'Meeting',
|
||||
allday: false,
|
||||
uid: "6045c603-11ab-43c5-bc30-0641420bb3a8",
|
||||
description: "Discuss project",
|
||||
location: "Zoom",
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
timezone: "Europe/Paris",
|
||||
transp: "TRANSPARENT",
|
||||
};
|
||||
uid: '6045c603-11ab-43c5-bc30-0641420bb3a8',
|
||||
description: 'Discuss project',
|
||||
location: 'Zoom',
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
timezone: 'Europe/Paris',
|
||||
transp: 'TRANSPARENT'
|
||||
}
|
||||
|
||||
// Fill inputs
|
||||
fireEvent.change(screen.getByLabelText("event.form.title"), {
|
||||
target: { value: newEvent.title },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('event.form.title'), {
|
||||
target: { value: newEvent.title }
|
||||
})
|
||||
// Click "Add description" button first
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "event.form.addDescription" })
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("event.form.description"), {
|
||||
target: { value: newEvent.description },
|
||||
});
|
||||
screen.getByRole('button', { name: 'event.form.addDescription' })
|
||||
)
|
||||
fireEvent.change(screen.getByLabelText('event.form.description'), {
|
||||
target: { value: newEvent.description }
|
||||
})
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "event.form.locationPlaceholder" })
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("event.form.location"), {
|
||||
target: { value: newEvent.location },
|
||||
});
|
||||
screen.getByRole('button', { name: 'event.form.locationPlaceholder' })
|
||||
)
|
||||
fireEvent.change(screen.getByLabelText('event.form.location'), {
|
||||
target: { value: newEvent.location }
|
||||
})
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const receivedPayload = spy.mock.calls[0][0];
|
||||
const receivedPayload = spy.mock.calls[0][0]
|
||||
|
||||
expect(receivedPayload.cal).toEqual(
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
||||
);
|
||||
preloadedState.calendars.list['667037022b752d0026472254/cal1']
|
||||
)
|
||||
|
||||
expect(receivedPayload.newEvent.title).toBe(newEvent.title);
|
||||
expect(receivedPayload.newEvent.description).toBe(newEvent.description);
|
||||
expect(receivedPayload.newEvent.location).toBe(newEvent.location);
|
||||
expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer);
|
||||
expect(receivedPayload.newEvent.title).toBe(newEvent.title)
|
||||
expect(receivedPayload.newEvent.description).toBe(newEvent.description)
|
||||
expect(receivedPayload.newEvent.location).toBe(newEvent.location)
|
||||
expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer)
|
||||
expect(receivedPayload.newEvent.color).toEqual(
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].color
|
||||
);
|
||||
preloadedState.calendars.list['667037022b752d0026472254/cal1'].color
|
||||
)
|
||||
|
||||
// onClose should be called
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("calls onClose with refresh = false when Cancel clicked", () => {
|
||||
renderPopover();
|
||||
it('calls onClose with refresh = false when Cancel clicked', () => {
|
||||
renderPopover()
|
||||
|
||||
// Cancel button only appears in expanded mode
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Cancel/i }))
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledWith(false);
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it("BUGFIX: Prefill Calendar field", async () => {
|
||||
renderPopover();
|
||||
it('BUGFIX: Prefill Calendar field', async () => {
|
||||
renderPopover()
|
||||
// In normal mode calendar is a SectionPreviewRow (button) with calendar name
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Calendar 1" })
|
||||
).toHaveTextContent("Calendar 1")
|
||||
);
|
||||
});
|
||||
screen.getByRole('button', { name: 'Calendar 1' })
|
||||
).toHaveTextContent('Calendar 1')
|
||||
)
|
||||
})
|
||||
|
||||
describe("EventModal - Drag and Drop Scenarios", () => {
|
||||
describe('EventModal - Drag and Drop Scenarios', () => {
|
||||
// Test 2.1: Drag from allday slot (single day)
|
||||
it("sets allday=true when drag from allday slot (single day)", async () => {
|
||||
it('sets allday=true when drag from allday slot (single day)', async () => {
|
||||
const selectedRange = {
|
||||
startStr: "2025-07-18",
|
||||
endStr: "2025-07-19",
|
||||
start: new Date("2025-07-18"),
|
||||
end: new Date("2025-07-19"),
|
||||
allDay: true,
|
||||
} as unknown as DateSelectArg;
|
||||
startStr: '2025-07-18',
|
||||
endStr: '2025-07-19',
|
||||
start: new Date('2025-07-18'),
|
||||
end: new Date('2025-07-19'),
|
||||
allDay: true
|
||||
} as unknown as DateSelectArg
|
||||
|
||||
renderPopover(selectedRange);
|
||||
renderPopover(selectedRange)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
const allDayCheckbox = screen.getByLabelText('event.form.allDay')
|
||||
await waitFor(() => {
|
||||
expect(allDayCheckbox).toBeChecked();
|
||||
});
|
||||
});
|
||||
expect(allDayCheckbox).toBeChecked()
|
||||
})
|
||||
})
|
||||
|
||||
// Test 2.2: Drag from allday slot (multiple days)
|
||||
it("sets allday=true and shows 2 date fields when drag from allday slot (multiple days)", async () => {
|
||||
it('sets allday=true and shows 2 date fields when drag from allday slot (multiple days)', async () => {
|
||||
const selectedRange = {
|
||||
startStr: "2025-07-18",
|
||||
endStr: "2025-07-21",
|
||||
start: new Date("2025-07-18"),
|
||||
end: new Date("2025-07-21"),
|
||||
allDay: true,
|
||||
} as unknown as DateSelectArg;
|
||||
startStr: '2025-07-18',
|
||||
endStr: '2025-07-21',
|
||||
start: new Date('2025-07-18'),
|
||||
end: new Date('2025-07-21'),
|
||||
allDay: true
|
||||
} as unknown as DateSelectArg
|
||||
|
||||
renderPopover(selectedRange);
|
||||
renderPopover(selectedRange)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
const allDayCheckbox = screen.getByLabelText('event.form.allDay')
|
||||
await waitFor(() => {
|
||||
expect(allDayCheckbox).toBeChecked();
|
||||
});
|
||||
expect(allDayCheckbox).toBeChecked()
|
||||
})
|
||||
|
||||
// Verify only date fields are shown
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("start-date-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("end-date-input")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("start-time-input")
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("end-time-input")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('start-date-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('end-date-input')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('start-time-input')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('end-time-input')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Test 2.3: Drag from week view (single day)
|
||||
it("keeps allday=false when drag from week view (single day)", async () => {
|
||||
it('keeps allday=false when drag from week view (single day)', async () => {
|
||||
const selectedRange = {
|
||||
startStr: "2025-07-18T09:00",
|
||||
endStr: "2025-07-18T10:00",
|
||||
start: new Date("2025-07-18T09:00"),
|
||||
end: new Date("2025-07-18T10:00"),
|
||||
allDay: false,
|
||||
} as unknown as DateSelectArg;
|
||||
startStr: '2025-07-18T09:00',
|
||||
endStr: '2025-07-18T10:00',
|
||||
start: new Date('2025-07-18T09:00'),
|
||||
end: new Date('2025-07-18T10:00'),
|
||||
allDay: false
|
||||
} as unknown as DateSelectArg
|
||||
|
||||
renderPopover(selectedRange);
|
||||
renderPopover(selectedRange)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
const allDayCheckbox = screen.getByLabelText('event.form.allDay')
|
||||
await waitFor(() => {
|
||||
expect(allDayCheckbox).not.toBeChecked();
|
||||
});
|
||||
expect(allDayCheckbox).not.toBeChecked()
|
||||
})
|
||||
|
||||
// Verify time fields are shown
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("start-time-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("end-time-input")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('start-time-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('end-time-input')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Test 2.4: Drag from week view (multiple days) - CRITICAL TEST
|
||||
it("keeps allday=false and shows 4 fields when drag from week view (multiple days)", async () => {
|
||||
it('keeps allday=false and shows 4 fields when drag from week view (multiple days)', async () => {
|
||||
const selectedRange = {
|
||||
startStr: "2025-07-18T09:00",
|
||||
endStr: "2025-07-20T10:00",
|
||||
start: new Date("2025-07-18T09:00"),
|
||||
end: new Date("2025-07-20T10:00"),
|
||||
allDay: false,
|
||||
} as unknown as DateSelectArg;
|
||||
startStr: '2025-07-18T09:00',
|
||||
endStr: '2025-07-20T10:00',
|
||||
start: new Date('2025-07-18T09:00'),
|
||||
end: new Date('2025-07-20T10:00'),
|
||||
allDay: false
|
||||
} as unknown as DateSelectArg
|
||||
|
||||
renderPopover(selectedRange);
|
||||
renderPopover(selectedRange)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
const allDayCheckbox = screen.getByLabelText('event.form.allDay')
|
||||
await waitFor(() => {
|
||||
expect(allDayCheckbox).not.toBeChecked(); // CRITICAL: should NOT be checked
|
||||
});
|
||||
expect(allDayCheckbox).not.toBeChecked() // CRITICAL: should NOT be checked
|
||||
})
|
||||
|
||||
// Verify all 4 fields are shown
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("start-date-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("start-time-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("end-date-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("end-time-input")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('start-date-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('start-time-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('end-date-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('end-time-input')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Test 2.5: Drag from week view (multiple days) - fallback path
|
||||
it("handles drag from week view (multiple days) with Date objects only", async () => {
|
||||
it('handles drag from week view (multiple days) with Date objects only', async () => {
|
||||
const selectedRange = {
|
||||
start: new Date("2025-07-18T09:00"),
|
||||
end: new Date("2025-07-20T10:00"),
|
||||
allDay: false,
|
||||
} as unknown as DateSelectArg;
|
||||
start: new Date('2025-07-18T09:00'),
|
||||
end: new Date('2025-07-20T10:00'),
|
||||
allDay: false
|
||||
} as unknown as DateSelectArg
|
||||
|
||||
renderPopover(selectedRange);
|
||||
renderPopover(selectedRange)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
const allDayCheckbox = screen.getByLabelText('event.form.allDay')
|
||||
await waitFor(() => {
|
||||
expect(allDayCheckbox).not.toBeChecked();
|
||||
});
|
||||
expect(allDayCheckbox).not.toBeChecked()
|
||||
})
|
||||
|
||||
// Verify all 4 fields are shown
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("start-date-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("start-time-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("end-date-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("end-time-input")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('start-date-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('start-time-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('end-date-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('end-time-input')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
import { makeRecurrenceString } from "@/features/Events/EventPreview/utils/makeRecurrenceString";
|
||||
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
||||
import { makeRecurrenceString } from '@/features/Events/EventPreview/utils/makeRecurrenceString'
|
||||
import { RepetitionObject } from '@/features/Events/EventsTypes'
|
||||
|
||||
describe("makeRecurrenceString", () => {
|
||||
describe('makeRecurrenceString', () => {
|
||||
const mockT = jest.fn((key: string, params?: any) => {
|
||||
if (params) {
|
||||
if (key === "eventPreview.everyInterval") {
|
||||
return `Every ${params.interval} ${params.unit}`;
|
||||
if (key === 'eventPreview.everyInterval') {
|
||||
return `Every ${params.interval} ${params.unit}`
|
||||
}
|
||||
if (key === "eventPreview.recurrenceOnDays") {
|
||||
return `on ${params.days}`;
|
||||
if (key === 'eventPreview.recurrenceOnDays') {
|
||||
return `on ${params.days}`
|
||||
}
|
||||
if (key === "eventPreview.forOccurrences") {
|
||||
return `for ${params.count} times`;
|
||||
if (key === 'eventPreview.forOccurrences') {
|
||||
return `for ${params.count} times`
|
||||
}
|
||||
if (key === "eventPreview.until") {
|
||||
return `until ${params.date}`;
|
||||
if (key === 'eventPreview.until') {
|
||||
return `until ${params.date}`
|
||||
}
|
||||
}
|
||||
|
||||
const translations: Record<string, string> = {
|
||||
"event.repeat.frequency.days": "days",
|
||||
"event.repeat.frequency.weeks": "weeks",
|
||||
"event.repeat.frequency.months": "months",
|
||||
"event.repeat.frequency.years": "years",
|
||||
"eventPreview.onDays.MO": "Monday",
|
||||
"eventPreview.onDays.TU": "Tuesday",
|
||||
"eventPreview.onDays.WE": "Wednesday",
|
||||
"eventPreview.onDays.TH": "Thursday",
|
||||
"eventPreview.onDays.FR": "Friday",
|
||||
"eventPreview.onDays.SA": "Saturday",
|
||||
"eventPreview.onDays.SU": "Sunday",
|
||||
locale: "en-US",
|
||||
};
|
||||
'event.repeat.frequency.days': 'days',
|
||||
'event.repeat.frequency.weeks': 'weeks',
|
||||
'event.repeat.frequency.months': 'months',
|
||||
'event.repeat.frequency.years': 'years',
|
||||
'eventPreview.onDays.MO': 'Monday',
|
||||
'eventPreview.onDays.TU': 'Tuesday',
|
||||
'eventPreview.onDays.WE': 'Wednesday',
|
||||
'eventPreview.onDays.TH': 'Thursday',
|
||||
'eventPreview.onDays.FR': 'Friday',
|
||||
'eventPreview.onDays.SA': 'Saturday',
|
||||
'eventPreview.onDays.SU': 'Sunday',
|
||||
locale: 'en-US'
|
||||
}
|
||||
|
||||
return translations[key] || key;
|
||||
});
|
||||
return translations[key] || key
|
||||
})
|
||||
|
||||
const startText = "Repeats";
|
||||
const startText = 'Repeats'
|
||||
|
||||
beforeEach(() => {
|
||||
mockT.mockClear();
|
||||
});
|
||||
mockT.mockClear()
|
||||
})
|
||||
|
||||
it("formats string for interval === 1 when enableStrForOneTimeInterval is true", () => {
|
||||
const repetition: RepetitionObject = { freq: "daily", interval: 1 };
|
||||
it('formats string for interval === 1 when enableStrForOneTimeInterval is true', () => {
|
||||
const repetition: RepetitionObject = { freq: 'daily', interval: 1 }
|
||||
const result = makeRecurrenceString({
|
||||
repetition,
|
||||
t: mockT,
|
||||
startText,
|
||||
enableStrForOneTimeInterval: true,
|
||||
});
|
||||
enableStrForOneTimeInterval: true
|
||||
})
|
||||
|
||||
expect(result).toBe("Repeats, days");
|
||||
});
|
||||
expect(result).toBe('Repeats, days')
|
||||
})
|
||||
|
||||
it("formats string when interval is undefined", () => {
|
||||
const repetition: RepetitionObject = { freq: "daily" };
|
||||
it('formats string when interval is undefined', () => {
|
||||
const repetition: RepetitionObject = { freq: 'daily' }
|
||||
const result = makeRecurrenceString({
|
||||
repetition,
|
||||
t: mockT,
|
||||
startText,
|
||||
enableStrForOneTimeInterval: true,
|
||||
});
|
||||
enableStrForOneTimeInterval: true
|
||||
})
|
||||
|
||||
expect(result).toBe("Repeats, days");
|
||||
});
|
||||
expect(result).toBe('Repeats, days')
|
||||
})
|
||||
|
||||
it("formats string for interval > 1", () => {
|
||||
const repetition: RepetitionObject = { freq: "weekly", interval: 2 };
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText });
|
||||
it('formats string for interval > 1', () => {
|
||||
const repetition: RepetitionObject = { freq: 'weekly', interval: 2 }
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText })
|
||||
|
||||
expect(result).toBe("Repeats, Every 2 weeks");
|
||||
expect(mockT).toHaveBeenCalledWith("eventPreview.everyInterval", {
|
||||
expect(result).toBe('Repeats, Every 2 weeks')
|
||||
expect(mockT).toHaveBeenCalledWith('eventPreview.everyInterval', {
|
||||
interval: 2,
|
||||
unit: "weeks",
|
||||
});
|
||||
});
|
||||
unit: 'weeks'
|
||||
})
|
||||
})
|
||||
|
||||
it("formats string with specific days and sorts them according to WEEK_DAYS order", () => {
|
||||
it('formats string with specific days and sorts them according to WEEK_DAYS order', () => {
|
||||
const repetition: RepetitionObject = {
|
||||
freq: "weekly",
|
||||
freq: 'weekly',
|
||||
interval: 1,
|
||||
byday: ["WE", "MO", "FR"],
|
||||
};
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText });
|
||||
byday: ['WE', 'MO', 'FR']
|
||||
}
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText })
|
||||
|
||||
// Expected order: MO, WE, FR
|
||||
expect(result).toBe("Repeats, on Monday, Wednesday, Friday");
|
||||
});
|
||||
expect(result).toBe('Repeats, on Monday, Wednesday, Friday')
|
||||
})
|
||||
|
||||
it("formats string with occurrences count", () => {
|
||||
it('formats string with occurrences count', () => {
|
||||
const repetition: RepetitionObject = {
|
||||
freq: "daily",
|
||||
freq: 'daily',
|
||||
interval: 1,
|
||||
occurrences: 5,
|
||||
};
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText });
|
||||
occurrences: 5
|
||||
}
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText })
|
||||
|
||||
expect(result).toBe("Repeats, for 5 times");
|
||||
});
|
||||
expect(result).toBe('Repeats, for 5 times')
|
||||
})
|
||||
|
||||
it("formats string with end date", () => {
|
||||
const endDate = "2025-12-31T00:00:00.000Z";
|
||||
it('formats string with end date', () => {
|
||||
const endDate = '2025-12-31T00:00:00.000Z'
|
||||
const repetition: RepetitionObject = {
|
||||
freq: "daily",
|
||||
freq: 'daily',
|
||||
interval: 1,
|
||||
endDate,
|
||||
};
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText });
|
||||
endDate
|
||||
}
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText })
|
||||
|
||||
const formattedDate = new Date(endDate).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const formattedDate = new Date(endDate).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
expect(result).toBe(`Repeats, until ${formattedDate}`);
|
||||
});
|
||||
expect(result).toBe(`Repeats, until ${formattedDate}`)
|
||||
})
|
||||
|
||||
it("combines multiple properties", () => {
|
||||
const endDate = "2025-12-31T00:00:00.000Z";
|
||||
it('combines multiple properties', () => {
|
||||
const endDate = '2025-12-31T00:00:00.000Z'
|
||||
const repetition: RepetitionObject = {
|
||||
freq: "monthly",
|
||||
freq: 'monthly',
|
||||
interval: 3,
|
||||
byday: ["TU", "TH"],
|
||||
endDate,
|
||||
};
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText });
|
||||
byday: ['TU', 'TH'],
|
||||
endDate
|
||||
}
|
||||
const result = makeRecurrenceString({ repetition, t: mockT, startText })
|
||||
|
||||
const formattedDate = new Date(endDate).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const formattedDate = new Date(endDate).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
expect(result).toBe(
|
||||
`Repeats, Every 3 months, on Tuesday, Thursday, until ${formattedDate}`
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("uses custom joinChar when provided", () => {
|
||||
it('uses custom joinChar when provided', () => {
|
||||
const repetition: RepetitionObject = {
|
||||
freq: "weekly",
|
||||
freq: 'weekly',
|
||||
interval: 2,
|
||||
occurrences: 10,
|
||||
};
|
||||
occurrences: 10
|
||||
}
|
||||
const result = makeRecurrenceString({
|
||||
repetition,
|
||||
t: mockT,
|
||||
startText,
|
||||
joinChar: ";",
|
||||
});
|
||||
joinChar: ';'
|
||||
})
|
||||
|
||||
expect(result).toBe("Repeats; Every 2 weeks; for 10 times");
|
||||
});
|
||||
});
|
||||
expect(result).toBe('Repeats; Every 2 weeks; for 10 times')
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +1,81 @@
|
||||
import * as CalendarApi from "@/features/Calendars/CalendarApi";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import * as EventApi from "@/features/Events/EventApi";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import * as CalendarApi from '@/features/Calendars/CalendarApi'
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import * as EventApi from '@/features/Events/EventApi'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import EventUpdateModal from '@/features/Events/EventUpdateModal'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/features/Events/EventApi");
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock("@/features/Events/api/updateSeries");
|
||||
jest.mock('@/features/Events/EventApi')
|
||||
jest.mock('@/features/Calendars/CalendarApi')
|
||||
jest.mock('@/features/Events/api/updateSeries')
|
||||
|
||||
describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
jest.restoreAllMocks()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
const baseUID = "recurring-event-base";
|
||||
const calId = "667037022b752d0026472254/cal1";
|
||||
const baseUID = 'recurring-event-base'
|
||||
const calId = '667037022b752d0026472254/cal1'
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "test-sid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'test-sid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
[calId]: {
|
||||
id: calId,
|
||||
name: "Test Calendar",
|
||||
color: "#FF0000",
|
||||
name: 'Test Calendar',
|
||||
color: '#FF0000',
|
||||
events: {},
|
||||
owner: { emails: ["test@test.com"] },
|
||||
},
|
||||
owner: { emails: ['test@test.com'] }
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
describe("Master Event Display", () => {
|
||||
describe('Master Event Display', () => {
|
||||
it("should fetch and display master event when editing 'all events' of a recurring series", async () => {
|
||||
// Given a recurring series with modified instances
|
||||
const masterEvent = {
|
||||
uid: baseUID,
|
||||
title: "Master Event Title",
|
||||
description: "Master Description",
|
||||
title: 'Master Event Title',
|
||||
description: 'Master Description',
|
||||
calId,
|
||||
start: "2025-01-15T10:00:00.000Z",
|
||||
end: "2025-01-15T11:00:00.000Z",
|
||||
repetition: { freq: "daily", interval: 1 },
|
||||
start: '2025-01-15T10:00:00.000Z',
|
||||
end: '2025-01-15T11:00:00.000Z',
|
||||
repetition: { freq: 'daily', interval: 1 },
|
||||
allday: false,
|
||||
timezone: "America/New_York",
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
} as CalendarEvent;
|
||||
timezone: 'America/New_York',
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }],
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`
|
||||
} as CalendarEvent
|
||||
|
||||
// Instance that was moved to different time
|
||||
const modifiedInstance = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250116`,
|
||||
title: "Modified Instance Title", // Different title
|
||||
start: "2025-01-16T14:00:00.000Z", // Different time (2PM instead of 10AM)
|
||||
end: "2025-01-16T15:00:00.000Z",
|
||||
};
|
||||
title: 'Modified Instance Title', // Different title
|
||||
start: '2025-01-16T14:00:00.000Z', // Different time (2PM instead of 10AM)
|
||||
end: '2025-01-16T15:00:00.000Z'
|
||||
}
|
||||
|
||||
const stateWithRecurringEvent = {
|
||||
...preloadedState,
|
||||
@@ -86,17 +86,17 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
...preloadedState.calendars.list[calId],
|
||||
events: {
|
||||
[baseUID]: masterEvent,
|
||||
[`${baseUID}/20250116`]: modifiedInstance,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250116`]: modifiedInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock getEvent to return master event
|
||||
const mockGetEvent = jest
|
||||
.spyOn(EventApi, "getEvent")
|
||||
.mockResolvedValue(masterEvent);
|
||||
.spyOn(EventApi, 'getEvent')
|
||||
.mockResolvedValue(masterEvent)
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -107,51 +107,51 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithRecurringEvent
|
||||
);
|
||||
)
|
||||
|
||||
// Wait for master event to be fetched
|
||||
await waitFor(() => {
|
||||
expect(mockGetEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
uid: baseUID, // Should fetch base UID, not instance
|
||||
uid: baseUID // Should fetch base UID, not instance
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
// Should display master event data, NOT modified instance data
|
||||
await waitFor(() => {
|
||||
const titleInput = screen.getByDisplayValue("Master Event Title");
|
||||
expect(titleInput).toBeInTheDocument();
|
||||
});
|
||||
const titleInput = screen.getByDisplayValue('Master Event Title')
|
||||
expect(titleInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should NOT show modified instance title
|
||||
expect(
|
||||
screen.queryByDisplayValue("Modified Instance Title")
|
||||
).not.toBeInTheDocument();
|
||||
screen.queryByDisplayValue('Modified Instance Title')
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
// Expand more options to show timezone (normal mode shows summary first)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
// Verify timezone dropdown shows master timezone
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue(/New York/i)).toBeDefined();
|
||||
});
|
||||
});
|
||||
expect(screen.getByDisplayValue(/New York/i)).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it("should use master event directly if clicked event is already the master", async () => {
|
||||
it('should use master event directly if clicked event is already the master', async () => {
|
||||
const masterEvent = {
|
||||
uid: baseUID, // No recurrence-id
|
||||
title: "Master Event",
|
||||
title: 'Master Event',
|
||||
calId,
|
||||
start: "2025-01-15T10:00:00.000Z",
|
||||
end: "2025-01-15T11:00:00.000Z",
|
||||
repetition: { freq: "weekly", interval: 1 },
|
||||
start: '2025-01-15T10:00:00.000Z',
|
||||
end: '2025-01-15T11:00:00.000Z',
|
||||
repetition: { freq: 'weekly', interval: 1 },
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
};
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`
|
||||
}
|
||||
|
||||
const stateWithMaster = {
|
||||
...preloadedState,
|
||||
@@ -161,14 +161,14 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
[calId]: {
|
||||
...preloadedState.calendars.list[calId],
|
||||
events: {
|
||||
[baseUID]: masterEvent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[baseUID]: masterEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockGetEvent = jest.spyOn(EventApi, "getEvent");
|
||||
const mockGetEvent = jest.spyOn(EventApi, 'getEvent')
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -179,28 +179,28 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithMaster
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Master Event")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue('Master Event')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should NOT fetch from API since we already have master
|
||||
expect(mockGetEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockGetEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should fallback to instance data if master event fetch fails", async () => {
|
||||
it('should fallback to instance data if master event fetch fails', async () => {
|
||||
const instance = {
|
||||
uid: `${baseUID}/20250115`,
|
||||
title: "Instance Event",
|
||||
title: 'Instance Event',
|
||||
calId,
|
||||
start: "2025-01-15T10:00:00.000Z",
|
||||
end: "2025-01-15T11:00:00.000Z",
|
||||
repetition: { freq: "daily", interval: 1 },
|
||||
start: '2025-01-15T10:00:00.000Z',
|
||||
end: '2025-01-15T11:00:00.000Z',
|
||||
repetition: { freq: 'daily', interval: 1 },
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
} as CalendarEvent;
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`
|
||||
} as CalendarEvent
|
||||
|
||||
const stateWithInstance = {
|
||||
...preloadedState,
|
||||
@@ -210,19 +210,19 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
[calId]: {
|
||||
...preloadedState.calendars.list[calId],
|
||||
events: {
|
||||
[`${baseUID}/20250115`]: instance,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250115`]: instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock getEvent to fail
|
||||
jest
|
||||
.spyOn(EventApi, "getEvent")
|
||||
.mockRejectedValue(new Error("Network error"));
|
||||
.spyOn(EventApi, 'getEvent')
|
||||
.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -233,48 +233,48 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithInstance
|
||||
);
|
||||
)
|
||||
|
||||
// Should still display the instance data as fallback
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Instance Event")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue('Instance Event')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to fetch master event:",
|
||||
'Failed to fetch master event:',
|
||||
expect.any(Error)
|
||||
);
|
||||
)
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Update All Events - Using Base UID", () => {
|
||||
it("should use base UID (not instance UID) when updating series with property changes", async () => {
|
||||
describe('Update All Events - Using Base UID', () => {
|
||||
it('should use base UID (not instance UID) when updating series with property changes', async () => {
|
||||
const masterEvent = {
|
||||
uid: baseUID,
|
||||
title: "Weekly Meeting",
|
||||
description: "Original description",
|
||||
title: 'Weekly Meeting',
|
||||
description: 'Original description',
|
||||
calId,
|
||||
start: "2025-01-15T10:00:00.000Z",
|
||||
end: "2025-01-15T11:00:00.000Z",
|
||||
repetition: { freq: "weekly", interval: 1 },
|
||||
start: '2025-01-15T10:00:00.000Z',
|
||||
end: '2025-01-15T11:00:00.000Z',
|
||||
repetition: { freq: 'weekly', interval: 1 },
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
} as CalendarEvent;
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`
|
||||
} as CalendarEvent
|
||||
|
||||
const instance1 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250115`,
|
||||
};
|
||||
uid: `${baseUID}/20250115`
|
||||
}
|
||||
|
||||
const instance2 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250122`,
|
||||
start: "2025-01-22T10:00:00.000Z",
|
||||
end: "2025-01-22T11:00:00.000Z",
|
||||
};
|
||||
start: '2025-01-22T10:00:00.000Z',
|
||||
end: '2025-01-22T11:00:00.000Z'
|
||||
}
|
||||
|
||||
const stateWithSeries = {
|
||||
...preloadedState,
|
||||
@@ -286,16 +286,16 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
events: {
|
||||
[baseUID]: masterEvent,
|
||||
[`${baseUID}/20250115`]: instance1,
|
||||
[`${baseUID}/20250122`]: instance2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250122`]: instance2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock getEvent to return master
|
||||
jest.spyOn(EventApi, "getEvent").mockResolvedValue(masterEvent);
|
||||
const updateSeriesAsyncSpy = jest.spyOn(eventThunks, "updateSeriesAsync");
|
||||
jest.spyOn(EventApi, 'getEvent').mockResolvedValue(masterEvent)
|
||||
const updateSeriesAsyncSpy = jest.spyOn(eventThunks, 'updateSeriesAsync')
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -306,61 +306,61 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithSeries
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Weekly Meeting")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue('Weekly Meeting')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Change only the title (property change, not recurrence rules)
|
||||
const titleInput = screen.getByDisplayValue("Weekly Meeting");
|
||||
fireEvent.change(titleInput, { target: { value: "Updated Meeting" } });
|
||||
const titleInput = screen.getByDisplayValue('Weekly Meeting')
|
||||
fireEvent.change(titleInput, { target: { value: 'Updated Meeting' } })
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
fireEvent.click(saveButton)
|
||||
})
|
||||
|
||||
// Verify updateSeriesAsync was dispatched with base UID
|
||||
await waitFor(() => {
|
||||
expect(updateSeriesAsyncSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
uid: "recurring-event-base",
|
||||
recurrenceId: undefined,
|
||||
uid: 'recurring-event-base',
|
||||
recurrenceId: undefined
|
||||
}),
|
||||
removeOverrides: false,
|
||||
removeOverrides: false
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should use base UID when updating series with recurrence rule changes", async () => {
|
||||
it('should use base UID when updating series with recurrence rule changes', async () => {
|
||||
const masterEvent = {
|
||||
uid: baseUID,
|
||||
title: "Daily Standup",
|
||||
title: 'Daily Standup',
|
||||
calId,
|
||||
start: "2025-01-15T09:00:00.000Z",
|
||||
end: "2025-01-15T09:30:00.000Z",
|
||||
repetition: { freq: "daily", interval: 1 }, // Daily
|
||||
start: '2025-01-15T09:00:00.000Z',
|
||||
end: '2025-01-15T09:30:00.000Z',
|
||||
repetition: { freq: 'daily', interval: 1 }, // Daily
|
||||
allday: false,
|
||||
timezone: "America/New_York",
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
} as CalendarEvent;
|
||||
timezone: 'America/New_York',
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`
|
||||
} as CalendarEvent
|
||||
|
||||
const instance1 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250115`,
|
||||
};
|
||||
uid: `${baseUID}/20250115`
|
||||
}
|
||||
|
||||
const instance2 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250116`,
|
||||
start: "2025-01-16T09:00:00.000Z",
|
||||
end: "2025-01-16T09:30:00.000Z",
|
||||
};
|
||||
start: '2025-01-16T09:00:00.000Z',
|
||||
end: '2025-01-16T09:30:00.000Z'
|
||||
}
|
||||
|
||||
const stateWithSeries = {
|
||||
...preloadedState,
|
||||
@@ -372,17 +372,17 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
events: {
|
||||
[baseUID]: masterEvent,
|
||||
[`${baseUID}/20250115`]: instance1,
|
||||
[`${baseUID}/20250116`]: instance2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250116`]: instance2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jest.spyOn(EventApi, "getEvent").mockResolvedValue(masterEvent);
|
||||
jest.spyOn(CalendarApi, "getCalendar").mockResolvedValue({
|
||||
_embedded: { "dav:item": [] },
|
||||
} as any);
|
||||
jest.spyOn(EventApi, 'getEvent').mockResolvedValue(masterEvent)
|
||||
jest.spyOn(CalendarApi, 'getCalendar').mockResolvedValue({
|
||||
_embedded: { 'dav:item': [] }
|
||||
} as any)
|
||||
|
||||
const { store } = renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -393,72 +393,72 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithSeries
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Daily Standup")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue('Daily Standup')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Click More Options to access repeat settings
|
||||
const moreOptionsButton = screen.getByText("common.moreOptions");
|
||||
fireEvent.click(moreOptionsButton);
|
||||
const moreOptionsButton = screen.getByText('common.moreOptions')
|
||||
fireEvent.click(moreOptionsButton)
|
||||
|
||||
await waitFor(() => {
|
||||
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
|
||||
expect(repeatCheckbox).toBeInTheDocument();
|
||||
expect(repeatCheckbox).toBeChecked();
|
||||
});
|
||||
const repeatCheckbox = screen.getByLabelText('event.form.repeat')
|
||||
expect(repeatCheckbox).toBeInTheDocument()
|
||||
expect(repeatCheckbox).toBeChecked()
|
||||
})
|
||||
|
||||
// Change from daily to weekly
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.days");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const weeklyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.weeks",
|
||||
});
|
||||
fireEvent.click(weeklyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.days')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const weeklyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.weeks'
|
||||
})
|
||||
fireEvent.click(weeklyOption)
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
fireEvent.click(saveButton)
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
})
|
||||
|
||||
// Verify all old instances were removed from store
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
const calendar = state.calendars.list[calId];
|
||||
const state = store.getState()
|
||||
const calendar = state.calendars.list[calId]
|
||||
|
||||
expect(calendar.events[baseUID]).toBeUndefined();
|
||||
expect(calendar.events[`${baseUID}/20250115`]).toBeUndefined();
|
||||
expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(calendar.events[baseUID]).toBeUndefined()
|
||||
expect(calendar.events[`${baseUID}/20250115`]).toBeUndefined()
|
||||
expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Master Event Date Preservation", () => {
|
||||
describe('Master Event Date Preservation', () => {
|
||||
it("should preserve master event date when updating time in 'all events' mode", async () => {
|
||||
// Master event starts on Jan 15 at 10:00 AM
|
||||
const masterEvent = {
|
||||
uid: baseUID,
|
||||
title: "Daily Standup",
|
||||
title: 'Daily Standup',
|
||||
calId,
|
||||
start: "2025-01-15T10:00:00.000Z",
|
||||
end: "2025-01-15T10:30:00.000Z",
|
||||
repetition: { freq: "daily", interval: 1 },
|
||||
start: '2025-01-15T10:00:00.000Z',
|
||||
end: '2025-01-15T10:30:00.000Z',
|
||||
repetition: { freq: 'daily', interval: 1 },
|
||||
allday: false,
|
||||
timezone: "UTC",
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
} as CalendarEvent;
|
||||
timezone: 'UTC',
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`
|
||||
} as CalendarEvent
|
||||
|
||||
// Instance on Jan 17 (different date)
|
||||
const instance = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250117`,
|
||||
start: "2025-01-17T10:00:00.000Z",
|
||||
end: "2025-01-17T10:30:00.000Z",
|
||||
};
|
||||
start: '2025-01-17T10:00:00.000Z',
|
||||
end: '2025-01-17T10:30:00.000Z'
|
||||
}
|
||||
|
||||
const stateWithSeries = {
|
||||
...preloadedState,
|
||||
@@ -469,19 +469,17 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
...preloadedState.calendars.list[calId],
|
||||
events: {
|
||||
[baseUID]: masterEvent,
|
||||
[`${baseUID}/20250117`]: instance,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250117`]: instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jest.spyOn(EventApi, "getEvent").mockResolvedValue(masterEvent);
|
||||
jest
|
||||
.spyOn(EventApi, "putEvent")
|
||||
.mockResolvedValue({ status: 201 } as any);
|
||||
jest.spyOn(EventApi, 'getEvent').mockResolvedValue(masterEvent)
|
||||
jest.spyOn(EventApi, 'putEvent').mockResolvedValue({ status: 201 } as any)
|
||||
|
||||
const updateSeriesAsyncSpy = jest.spyOn(eventThunks, "updateSeriesAsync");
|
||||
const updateSeriesAsyncSpy = jest.spyOn(eventThunks, 'updateSeriesAsync')
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -492,40 +490,40 @@ describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithSeries
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Daily Standup")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue('Daily Standup')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Expand more options to show date/time inputs (normal mode shows DateTimeSummary first)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
const input = await waitFor(() => screen.getByTestId("start-time-input"));
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
const input = await waitFor(() => screen.getByTestId('start-time-input'))
|
||||
// Change time to 2:00 PM (14:00)
|
||||
await userEvent.click(input);
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "14:00{enter}");
|
||||
await userEvent.click(input)
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, '14:00{enter}')
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
fireEvent.click(saveButton)
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
})
|
||||
|
||||
// Verify the API was called with master date (Jan 15) + new time (14:00)
|
||||
await waitFor(() => {
|
||||
expect(updateSeriesAsyncSpy).toHaveBeenCalled();
|
||||
const callArgs = updateSeriesAsyncSpy.mock.calls[0][0];
|
||||
const updatedEvent = callArgs.event;
|
||||
expect(updateSeriesAsyncSpy).toHaveBeenCalled()
|
||||
const callArgs = updateSeriesAsyncSpy.mock.calls[0][0]
|
||||
const updatedEvent = callArgs.event
|
||||
|
||||
// Should preserve Jan 15 date (master), not Jan 17 (clicked instance)
|
||||
expect(updatedEvent.start).toContain("2025-01-15");
|
||||
expect(updatedEvent.start).toContain('2025-01-15')
|
||||
// But with new time 14:00 (in UTC, this is 14:00)
|
||||
expect(updatedEvent.start).toContain("14:00");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(updatedEvent.start).toContain('14:00')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,329 +1,329 @@
|
||||
import * as CalendarApi from "@/features/Calendars/CalendarApi";
|
||||
import * as EventApi from "@/features/Events/EventApi";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import * as CalendarApi from '@/features/Calendars/CalendarApi'
|
||||
import * as EventApi from '@/features/Events/EventApi'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import EventUpdateModal from '@/features/Events/EventUpdateModal'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/features/Events/EventApi");
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock('@/features/Events/EventApi')
|
||||
jest.mock('@/features/Calendars/CalendarApi')
|
||||
|
||||
describe("EventUpdateModal Timezone Handling", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('EventUpdateModal Timezone Handling', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "test-sid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'test-sid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Test Calendar",
|
||||
color: "#FF0000",
|
||||
events: {},
|
||||
},
|
||||
'667037022b752d0026472254/cal1': {
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
name: 'Test Calendar',
|
||||
color: '#FF0000',
|
||||
events: {}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
it("displays event time in original timezone when editing event", async () => {
|
||||
it('displays event time in original timezone when editing event', async () => {
|
||||
// GIVEN user timezone is UTC+2
|
||||
// WHEN the user edits an event at 2PM UTC+7 (Asia/Bangkok)
|
||||
// THEN the update modal prompts him date with 2PM UTC+7
|
||||
|
||||
// Create event at 2PM UTC+7 (Asia/Bangkok)
|
||||
// 2PM UTC+7 = 14:00 in Bangkok = 07:00 UTC
|
||||
const eventDateUTC = new Date("2025-01-15T07:00:00.000Z");
|
||||
const eventDateUTC = new Date('2025-01-15T07:00:00.000Z')
|
||||
|
||||
const eventData = {
|
||||
uid: "test-event-1",
|
||||
title: "Timezone Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: 'test-event-1',
|
||||
title: 'Timezone Event',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: eventDateUTC.toISOString(),
|
||||
end: new Date(eventDateUTC.getTime() + 3600000).toISOString(),
|
||||
timezone: "Asia/Bangkok",
|
||||
timezone: 'Asia/Bangkok',
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
URL: "/calendars/667037022b752d0026472254/cal1/test-event-1.ics",
|
||||
} as CalendarEvent;
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }],
|
||||
URL: '/calendars/667037022b752d0026472254/cal1/test-event-1.ics'
|
||||
} as CalendarEvent
|
||||
|
||||
const stateWithEvent = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
'667037022b752d0026472254/cal1': {
|
||||
...preloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
||||
events: {
|
||||
"test-event-1": eventData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
'test-event-1': eventData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"test-event-1"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'test-event-1'}
|
||||
eventData={eventData}
|
||||
/>,
|
||||
stateWithEvent
|
||||
);
|
||||
)
|
||||
|
||||
// Verify title is displayed
|
||||
const titleInput = screen.getByDisplayValue("Timezone Event");
|
||||
expect(titleInput).toBeInTheDocument();
|
||||
const titleInput = screen.getByDisplayValue('Timezone Event')
|
||||
expect(titleInput).toBeInTheDocument()
|
||||
|
||||
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
|
||||
// Verify the start date and time inputs exist
|
||||
await waitFor(() => {
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const startDateInput = screen.getByTestId('start-date-input')
|
||||
const startTimeInput = screen.getByTestId('start-time-input')
|
||||
|
||||
expect(startDateInput).toBeInTheDocument();
|
||||
expect(startTimeInput).toBeInTheDocument();
|
||||
expect(startDateInput).toBeInTheDocument()
|
||||
expect(startTimeInput).toBeInTheDocument()
|
||||
|
||||
// MUI DatePicker/TimePicker values are stored differently - just check elements exist
|
||||
// The actual values are verified through the form submission
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves original timezone when editing event fields", async () => {
|
||||
const eventDateUTC = new Date("2025-01-15T07:00:00.000Z");
|
||||
it('preserves original timezone when editing event fields', async () => {
|
||||
const eventDateUTC = new Date('2025-01-15T07:00:00.000Z')
|
||||
|
||||
const eventData = {
|
||||
uid: "test-event-2",
|
||||
title: "Original Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: 'test-event-2',
|
||||
title: 'Original Event',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: eventDateUTC.toISOString(),
|
||||
end: new Date(eventDateUTC.getTime() + 3600000).toISOString(),
|
||||
timezone: "Asia/Bangkok",
|
||||
timezone: 'Asia/Bangkok',
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
URL: "/calendars/667037022b752d0026472254/cal1/test-event-2.ics",
|
||||
} as CalendarEvent;
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }],
|
||||
URL: '/calendars/667037022b752d0026472254/cal1/test-event-2.ics'
|
||||
} as CalendarEvent
|
||||
|
||||
const stateWithEvent = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
'667037022b752d0026472254/cal1': {
|
||||
...preloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
||||
events: {
|
||||
"test-event-2": eventData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
'test-event-2': eventData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"test-event-2"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'test-event-2'}
|
||||
eventData={eventData}
|
||||
/>,
|
||||
stateWithEvent
|
||||
);
|
||||
)
|
||||
|
||||
// Edit the title
|
||||
const titleInput = screen.getByDisplayValue("Original Event");
|
||||
fireEvent.change(titleInput, { target: { value: "Updated Event" } });
|
||||
const titleInput = screen.getByDisplayValue('Original Event')
|
||||
fireEvent.change(titleInput, { target: { value: 'Updated Event' } })
|
||||
|
||||
// Verify the timezone is still preserved (should be Asia/Bangkok)
|
||||
expect(titleInput).toHaveValue("Updated Event");
|
||||
});
|
||||
expect(titleInput).toHaveValue('Updated Event')
|
||||
})
|
||||
|
||||
it("preserves resources when editing an event", async () => {
|
||||
const eventDateUTC = new Date("2025-01-15T07:00:00.000Z");
|
||||
it('preserves resources when editing an event', async () => {
|
||||
const eventDateUTC = new Date('2025-01-15T07:00:00.000Z')
|
||||
|
||||
const eventData = {
|
||||
uid: "test-event-resource",
|
||||
title: "Resource Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: 'test-event-resource',
|
||||
title: 'Resource Event',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: eventDateUTC.toISOString(),
|
||||
end: new Date(eventDateUTC.getTime() + 3600000).toISOString(),
|
||||
timezone: "Asia/Bangkok",
|
||||
timezone: 'Asia/Bangkok',
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [
|
||||
{ cn: "test", cal_address: "test@test.com" },
|
||||
{ cn: 'test', cal_address: 'test@test.com' },
|
||||
{
|
||||
cn: "Conference Room",
|
||||
cal_address: "room@test.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "RESOURCE",
|
||||
},
|
||||
cn: 'Conference Room',
|
||||
cal_address: 'room@test.com',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'RESOURCE'
|
||||
}
|
||||
],
|
||||
URL: "/calendars/667037022b752d0026472254/cal1/test-event-resource.ics",
|
||||
} as CalendarEvent;
|
||||
URL: '/calendars/667037022b752d0026472254/cal1/test-event-resource.ics'
|
||||
} as CalendarEvent
|
||||
|
||||
const stateWithEvent = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
'667037022b752d0026472254/cal1': {
|
||||
...preloadedState.calendars.list['667037022b752d0026472254/cal1'],
|
||||
events: {
|
||||
"test-event-resource": eventData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
'test-event-resource': eventData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockPutEvent = jest.spyOn(EventApi, "putEvent").mockResolvedValue({
|
||||
const mockPutEvent = jest.spyOn(EventApi, 'putEvent').mockResolvedValue({
|
||||
status: 201,
|
||||
url: `/calendars/667037022b752d0026472254/cal1/test-event-resource.ics`,
|
||||
} as any);
|
||||
url: `/calendars/667037022b752d0026472254/cal1/test-event-resource.ics`
|
||||
} as any)
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"test-event-resource"}
|
||||
calId={'667037022b752d0026472254/cal1'}
|
||||
eventId={'test-event-resource'}
|
||||
eventData={eventData}
|
||||
/>,
|
||||
stateWithEvent
|
||||
);
|
||||
)
|
||||
|
||||
// Edit the title
|
||||
const titleInput = screen.getByDisplayValue("Resource Event");
|
||||
const titleInput = screen.getByDisplayValue('Resource Event')
|
||||
fireEvent.change(titleInput, {
|
||||
target: { value: "Updated Resource Event" },
|
||||
});
|
||||
target: { value: 'Updated Resource Event' }
|
||||
})
|
||||
|
||||
// Click Save
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
fireEvent.click(saveButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPutEvent).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockPutEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const putEventCall = mockPutEvent.mock.calls[0][0];
|
||||
expect(putEventCall.title).toBe("Updated Resource Event");
|
||||
const putEventCall = mockPutEvent.mock.calls[0][0]
|
||||
expect(putEventCall.title).toBe('Updated Resource Event')
|
||||
|
||||
// Check that the resource is still in the attendee list!
|
||||
const attendees = putEventCall.attendee;
|
||||
const resource = attendees.find((a: any) => a.cutype === "RESOURCE");
|
||||
expect(resource).toBeDefined();
|
||||
expect(resource!.cn).toBe("Conference Room");
|
||||
expect(resource!.cal_address).toBe("room@test.com");
|
||||
expect(resource!.partstat).toBe("ACCEPTED");
|
||||
expect(resource!.rsvp).toBe("TRUE");
|
||||
expect(resource!.role).toBe("REQ-PARTICIPANT");
|
||||
expect(resource!.cutype).toBe("RESOURCE");
|
||||
});
|
||||
});
|
||||
const attendees = putEventCall.attendee
|
||||
const resource = attendees.find((a: any) => a.cutype === 'RESOURCE')
|
||||
expect(resource).toBeDefined()
|
||||
expect(resource!.cn).toBe('Conference Room')
|
||||
expect(resource!.cal_address).toBe('room@test.com')
|
||||
expect(resource!.partstat).toBe('ACCEPTED')
|
||||
expect(resource!.rsvp).toBe('TRUE')
|
||||
expect(resource!.role).toBe('REQ-PARTICIPANT')
|
||||
expect(resource!.cutype).toBe('RESOURCE')
|
||||
})
|
||||
})
|
||||
|
||||
describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('EventUpdateModal Recurring to Non-Recurring Conversion', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
const baseUID = "recurring-event-base";
|
||||
const calId = "667037022b752d0026472254/cal1";
|
||||
const baseUID = 'recurring-event-base'
|
||||
const calId = '667037022b752d0026472254/cal1'
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "test-sid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'test-sid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
cn: 'test',
|
||||
cal_address: 'test@test.com'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
[calId]: {
|
||||
id: calId,
|
||||
name: "Test Calendar",
|
||||
color: "#FF0000",
|
||||
events: {},
|
||||
},
|
||||
name: 'Test Calendar',
|
||||
color: '#FF0000',
|
||||
events: {}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
it("converts recurring event to non-recurring when repeat is disabled in edit all mode", async () => {
|
||||
it('converts recurring event to non-recurring when repeat is disabled in edit all mode', async () => {
|
||||
// Create recurring event with multiple instances
|
||||
const eventDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
const eventDate = new Date('2025-01-15T10:00:00.000Z')
|
||||
|
||||
const masterEvent = {
|
||||
uid: baseUID,
|
||||
title: "Recurring Meeting",
|
||||
title: 'Recurring Meeting',
|
||||
calId,
|
||||
start: eventDate.toISOString(),
|
||||
end: new Date(eventDate.getTime() + 3600000).toISOString(),
|
||||
repetition: { freq: "daily", interval: 1 },
|
||||
repetition: { freq: 'daily', interval: 1 },
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }],
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
timezone: "UTC",
|
||||
} as CalendarEvent;
|
||||
timezone: 'UTC'
|
||||
} as CalendarEvent
|
||||
|
||||
const instance1 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250115`,
|
||||
start: "2025-01-15T10:00:00.000Z",
|
||||
end: "2025-01-15T11:00:00.000Z",
|
||||
};
|
||||
start: '2025-01-15T10:00:00.000Z',
|
||||
end: '2025-01-15T11:00:00.000Z'
|
||||
}
|
||||
|
||||
const instance2 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250116`,
|
||||
start: "2025-01-16T10:00:00.000Z",
|
||||
end: "2025-01-16T11:00:00.000Z",
|
||||
};
|
||||
start: '2025-01-16T10:00:00.000Z',
|
||||
end: '2025-01-16T11:00:00.000Z'
|
||||
}
|
||||
|
||||
const instance3 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250117`,
|
||||
start: "2025-01-17T10:00:00.000Z",
|
||||
end: "2025-01-17T11:00:00.000Z",
|
||||
};
|
||||
start: '2025-01-17T10:00:00.000Z',
|
||||
end: '2025-01-17T11:00:00.000Z'
|
||||
}
|
||||
|
||||
const stateWithRecurringEvent = {
|
||||
...preloadedState,
|
||||
@@ -336,29 +336,29 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
[baseUID]: masterEvent,
|
||||
[`${baseUID}/20250115`]: instance1,
|
||||
[`${baseUID}/20250116`]: instance2,
|
||||
[`${baseUID}/20250117`]: instance3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250117`]: instance3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock API calls
|
||||
const mockDeleteEvent = jest
|
||||
.spyOn(EventApi, "deleteEvent")
|
||||
.mockResolvedValue({} as any);
|
||||
const mockPutEvent = jest.spyOn(EventApi, "putEvent").mockResolvedValue({
|
||||
.spyOn(EventApi, 'deleteEvent')
|
||||
.mockResolvedValue({} as any)
|
||||
const mockPutEvent = jest.spyOn(EventApi, 'putEvent').mockResolvedValue({
|
||||
status: 201,
|
||||
url: `/calendars/${calId}/new-event.ics`,
|
||||
} as any);
|
||||
url: `/calendars/${calId}/new-event.ics`
|
||||
} as any)
|
||||
const mockGetMasterEvent = jest
|
||||
.spyOn(EventApi, "getEvent")
|
||||
.mockResolvedValue(masterEvent);
|
||||
jest.spyOn(CalendarApi, "getCalendar").mockResolvedValue({
|
||||
.spyOn(EventApi, 'getEvent')
|
||||
.mockResolvedValue(masterEvent)
|
||||
jest.spyOn(CalendarApi, 'getCalendar').mockResolvedValue({
|
||||
_embedded: {
|
||||
"dav:item": [],
|
||||
},
|
||||
} as any);
|
||||
'dav:item': []
|
||||
}
|
||||
} as any)
|
||||
|
||||
const { store } = renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -369,97 +369,97 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithRecurringEvent
|
||||
);
|
||||
)
|
||||
|
||||
// Wait for master event to be fetched AND form to be initialized
|
||||
await waitFor(() => {
|
||||
expect(mockGetMasterEvent).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockGetMasterEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
// Wait for the repeat checkbox to be checked (indicating form is initialized)
|
||||
const repeatCheckbox = await waitFor(() => {
|
||||
const checkbox = screen.getByLabelText("event.form.repeat");
|
||||
expect(checkbox).toBeChecked();
|
||||
return checkbox;
|
||||
});
|
||||
const checkbox = screen.getByLabelText('event.form.repeat')
|
||||
expect(checkbox).toBeChecked()
|
||||
return checkbox
|
||||
})
|
||||
|
||||
// Now uncheck repeat checkbox
|
||||
await act(async () => {
|
||||
fireEvent.click(repeatCheckbox);
|
||||
});
|
||||
fireEvent.click(repeatCheckbox)
|
||||
})
|
||||
|
||||
// Wait for checkbox to be unchecked
|
||||
await waitFor(() => {
|
||||
expect(repeatCheckbox).not.toBeChecked();
|
||||
});
|
||||
expect(repeatCheckbox).not.toBeChecked()
|
||||
})
|
||||
|
||||
// Click Save button
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
fireEvent.click(saveButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPutEvent).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockPutEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Verify API calls - should delete all instances
|
||||
await waitFor(
|
||||
() => {
|
||||
// Should have called deleteEvent for each instance (4 total: base + 3 recurrences)
|
||||
expect(mockDeleteEvent).toHaveBeenCalled();
|
||||
expect(mockDeleteEvent).toHaveBeenCalled()
|
||||
// At least one instance should be deleted
|
||||
expect(mockDeleteEvent.mock.calls.length).toBeGreaterThan(0);
|
||||
expect(mockDeleteEvent.mock.calls.length).toBeGreaterThan(0)
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
// Verify new event was created via putEvent
|
||||
expect(mockPutEvent).toHaveBeenCalled();
|
||||
const putEventCall = mockPutEvent.mock.calls[0][0];
|
||||
expect(putEventCall.title).toBe("Recurring Meeting");
|
||||
expect(putEventCall.repetition?.freq).toBeFalsy();
|
||||
expect(putEventCall.uid).not.toContain(baseUID);
|
||||
expect(mockPutEvent).toHaveBeenCalled()
|
||||
const putEventCall = mockPutEvent.mock.calls[0][0]
|
||||
expect(putEventCall.title).toBe('Recurring Meeting')
|
||||
expect(putEventCall.repetition?.freq).toBeFalsy()
|
||||
expect(putEventCall.uid).not.toContain(baseUID)
|
||||
|
||||
// Verify Redux store state changes
|
||||
const finalState = store.getState();
|
||||
const calendar = finalState.calendars.list[calId];
|
||||
const finalState = store.getState()
|
||||
const calendar = finalState.calendars.list[calId]
|
||||
|
||||
// Old recurring instances should be removed from store
|
||||
expect(calendar.events[baseUID]).toBeUndefined();
|
||||
expect(calendar.events[`${baseUID}/20250115`]).toBeUndefined();
|
||||
expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined();
|
||||
expect(calendar.events[`${baseUID}/20250117`]).toBeUndefined();
|
||||
expect(calendar.events[baseUID]).toBeUndefined()
|
||||
expect(calendar.events[`${baseUID}/20250115`]).toBeUndefined()
|
||||
expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined()
|
||||
expect(calendar.events[`${baseUID}/20250117`]).toBeUndefined()
|
||||
|
||||
// Verify modal was closed after completion
|
||||
await waitFor(() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it("gracefully handles deletion errors and continues to create new event", async () => {
|
||||
const eventDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
it('gracefully handles deletion errors and continues to create new event', async () => {
|
||||
const eventDate = new Date('2025-01-15T10:00:00.000Z')
|
||||
|
||||
const masterEvent = {
|
||||
uid: baseUID,
|
||||
title: "Recurring Meeting",
|
||||
title: 'Recurring Meeting',
|
||||
calId,
|
||||
start: eventDate.toISOString(),
|
||||
end: new Date(eventDate.getTime() + 3600000).toISOString(),
|
||||
repetition: { freq: "weekly", interval: 1 },
|
||||
repetition: { freq: 'weekly', interval: 1 },
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }],
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
timezone: "UTC",
|
||||
} as CalendarEvent;
|
||||
timezone: 'UTC'
|
||||
} as CalendarEvent
|
||||
|
||||
const instance1 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250115`,
|
||||
};
|
||||
uid: `${baseUID}/20250115`
|
||||
}
|
||||
|
||||
const stateWithRecurringEvent = {
|
||||
...preloadedState,
|
||||
@@ -470,24 +470,24 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
...preloadedState.calendars.list[calId],
|
||||
events: {
|
||||
[baseUID]: masterEvent,
|
||||
[`${baseUID}/20250115`]: instance1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250115`]: instance1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock deleteEvent to fail with a non-404 error
|
||||
const mockDeleteEvent = jest
|
||||
.spyOn(EventApi, "deleteEvent")
|
||||
.mockRejectedValue(new Error("Network error"));
|
||||
.spyOn(EventApi, 'deleteEvent')
|
||||
.mockRejectedValue(new Error('Network error'))
|
||||
const mockPutEvent = jest
|
||||
.spyOn(EventApi, "putEvent")
|
||||
.mockResolvedValue({ status: 201 } as any);
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
.spyOn(EventApi, 'putEvent')
|
||||
.mockResolvedValue({ status: 201 } as any)
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
const mockGetMasterEvent = jest
|
||||
.spyOn(EventApi, "getEvent")
|
||||
.mockResolvedValue(masterEvent);
|
||||
.spyOn(EventApi, 'getEvent')
|
||||
.mockResolvedValue(masterEvent)
|
||||
|
||||
const { store } = renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -498,106 +498,106 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithRecurringEvent
|
||||
);
|
||||
)
|
||||
|
||||
// Wait for master event to be fetched
|
||||
await waitFor(() => {
|
||||
expect(mockGetMasterEvent).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockGetMasterEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Wait for component to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue('Recurring Meeting')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
// Wait for repeat checkbox to be checked
|
||||
const repeatCheckbox = await waitFor(() => {
|
||||
const checkbox = screen.getByLabelText("event.form.repeat");
|
||||
expect(checkbox).toBeChecked();
|
||||
return checkbox;
|
||||
});
|
||||
const checkbox = screen.getByLabelText('event.form.repeat')
|
||||
expect(checkbox).toBeChecked()
|
||||
return checkbox
|
||||
})
|
||||
|
||||
// Uncheck repeat checkbox and save
|
||||
await act(async () => {
|
||||
fireEvent.click(repeatCheckbox);
|
||||
});
|
||||
fireEvent.click(repeatCheckbox)
|
||||
})
|
||||
|
||||
// Wait for checkbox to be unchecked
|
||||
await waitFor(() => {
|
||||
expect(repeatCheckbox).not.toBeChecked();
|
||||
});
|
||||
expect(repeatCheckbox).not.toBeChecked()
|
||||
})
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
fireEvent.click(saveButton)
|
||||
})
|
||||
|
||||
// Wait for delete to be attempted
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(mockDeleteEvent).toHaveBeenCalled();
|
||||
expect(mockDeleteEvent).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
// Verify error was logged for failed deletion
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to delete event file")
|
||||
);
|
||||
expect.stringContaining('Failed to delete event file')
|
||||
)
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
// New implementation gracefully handles deletion errors:
|
||||
// Even if some instances fail to delete, we still create the new event
|
||||
// This ensures user gets their non-recurring event
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(mockPutEvent).toHaveBeenCalled();
|
||||
expect(mockPutEvent).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
const putEventCall = mockPutEvent.mock.calls[0][0];
|
||||
expect(putEventCall.title).toBe("Recurring Meeting");
|
||||
expect(putEventCall.repetition?.freq).toBeFalsy();
|
||||
const putEventCall = mockPutEvent.mock.calls[0][0]
|
||||
expect(putEventCall.title).toBe('Recurring Meeting')
|
||||
expect(putEventCall.repetition?.freq).toBeFalsy()
|
||||
|
||||
// Modal should close after operation completes
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("closes all modals after converting recurring to non-recurring", async () => {
|
||||
const eventDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
it('closes all modals after converting recurring to non-recurring', async () => {
|
||||
const eventDate = new Date('2025-01-15T10:00:00.000Z')
|
||||
|
||||
const masterEvent = {
|
||||
uid: baseUID,
|
||||
title: "Recurring Meeting",
|
||||
title: 'Recurring Meeting',
|
||||
calId,
|
||||
start: eventDate.toISOString(),
|
||||
end: new Date(eventDate.getTime() + 3600000).toISOString(),
|
||||
repetition: { freq: "monthly", interval: 1 },
|
||||
repetition: { freq: 'monthly', interval: 1 },
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
||||
attendee: [{ cn: 'test', cal_address: 'test@test.com' }],
|
||||
URL: `/calendars/${calId}/${baseUID}.ics`,
|
||||
timezone: "UTC",
|
||||
} as CalendarEvent;
|
||||
timezone: 'UTC'
|
||||
} as CalendarEvent
|
||||
|
||||
const instance1 = {
|
||||
...masterEvent,
|
||||
uid: `${baseUID}/20250115`,
|
||||
};
|
||||
uid: `${baseUID}/20250115`
|
||||
}
|
||||
|
||||
const stateWithRecurringEvent = {
|
||||
...preloadedState,
|
||||
@@ -608,24 +608,24 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
...preloadedState.calendars.list[calId],
|
||||
events: {
|
||||
[baseUID]: masterEvent,
|
||||
[`${baseUID}/20250115`]: instance1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
[`${baseUID}/20250115`]: instance1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock API calls
|
||||
jest.spyOn(EventApi, "deleteEvent").mockResolvedValue({} as any);
|
||||
jest.spyOn(EventApi, "putEvent").mockResolvedValue({ status: 201 } as any);
|
||||
jest.spyOn(CalendarApi, "getCalendar").mockResolvedValue({
|
||||
jest.spyOn(EventApi, 'deleteEvent').mockResolvedValue({} as any)
|
||||
jest.spyOn(EventApi, 'putEvent').mockResolvedValue({ status: 201 } as any)
|
||||
jest.spyOn(CalendarApi, 'getCalendar').mockResolvedValue({
|
||||
_embedded: {
|
||||
"dav:item": [],
|
||||
},
|
||||
} as any);
|
||||
'dav:item': []
|
||||
}
|
||||
} as any)
|
||||
|
||||
// Mock onCloseAll to test closing both modals
|
||||
const mockOnCloseAll = jest.fn();
|
||||
const mockOnCloseAll = jest.fn()
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -637,36 +637,36 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
||||
typeOfAction="all"
|
||||
/>,
|
||||
stateWithRecurringEvent
|
||||
);
|
||||
)
|
||||
|
||||
// Wait for component to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Recurring Meeting")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue('Recurring Meeting')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.moreOptions' }))
|
||||
// Uncheck repeat and save
|
||||
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
|
||||
const repeatCheckbox = screen.getByLabelText('event.form.repeat')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(repeatCheckbox);
|
||||
});
|
||||
fireEvent.click(repeatCheckbox)
|
||||
})
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "actions.save" });
|
||||
const saveButton = screen.getByRole('button', { name: 'actions.save' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
fireEvent.click(saveButton)
|
||||
})
|
||||
|
||||
// Verify onCloseAll was called to close both preview and update modals
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(mockOnCloseAll).toHaveBeenCalled();
|
||||
expect(mockOnCloseAll).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
// Verify onClose was NOT called (onCloseAll is used instead)
|
||||
expect(mockOnClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(mockOnClose).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,95 +1,93 @@
|
||||
import {
|
||||
hasFreeBusyConflict,
|
||||
useAttendeesFreeBusy,
|
||||
} from "@/components/Attendees/useFreeBusy";
|
||||
import * as getFreeBusyREPORT from "@/features/Events/api/getFreeBusyForAddedAttendeesREPORT";
|
||||
import * as getFreeBusyPOST from "@/features/Events/api/getFreeBusyForEventAttendeesPOST";
|
||||
import * as getUserData from "@/features/Events/api/getUserDataFromEmail";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
useAttendeesFreeBusy
|
||||
} from '@/components/Attendees/useFreeBusy'
|
||||
import * as getFreeBusyREPORT from '@/features/Events/api/getFreeBusyForAddedAttendeesREPORT'
|
||||
import * as getFreeBusyPOST from '@/features/Events/api/getFreeBusyForEventAttendeesPOST'
|
||||
import * as getUserData from '@/features/Events/api/getUserDataFromEmail'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
|
||||
jest.mock("moment-timezone", () => {
|
||||
const actual = jest.requireActual("moment-timezone");
|
||||
return actual;
|
||||
});
|
||||
jest.mock('moment-timezone', () => {
|
||||
const actual = jest.requireActual('moment-timezone')
|
||||
return actual
|
||||
})
|
||||
|
||||
jest.mock("@/features/Events/api/getUserDataFromEmail");
|
||||
jest.mock("@/features/Events/api/getFreeBusyForAddedAttendeesREPORT");
|
||||
jest.mock("@/features/Events/api/getFreeBusyForEventAttendeesPOST");
|
||||
jest.mock('@/features/Events/api/getUserDataFromEmail')
|
||||
jest.mock('@/features/Events/api/getFreeBusyForAddedAttendeesREPORT')
|
||||
jest.mock('@/features/Events/api/getFreeBusyForEventAttendeesPOST')
|
||||
|
||||
const mockGetUserData = getUserData.getUserDataFromEmail as jest.MockedFunction<
|
||||
typeof getUserData.getUserDataFromEmail
|
||||
>;
|
||||
>
|
||||
const mockREPORT =
|
||||
getFreeBusyREPORT.getFreeBusyForAddedAttendeesREPORT as jest.MockedFunction<
|
||||
typeof getFreeBusyREPORT.getFreeBusyForAddedAttendeesREPORT
|
||||
>;
|
||||
>
|
||||
const mockPOST =
|
||||
getFreeBusyPOST.getFreeBusyForEventAttendeesPOST as jest.MockedFunction<
|
||||
typeof getFreeBusyPOST.getFreeBusyForEventAttendeesPOST
|
||||
>;
|
||||
>
|
||||
|
||||
const START = "2026-03-14T14:00:00";
|
||||
const END = "2026-03-14T15:00:00";
|
||||
const TZ = "Europe/Paris";
|
||||
const START = '2026-03-14T14:00:00'
|
||||
const END = '2026-03-14T15:00:00'
|
||||
const TZ = 'Europe/Paris'
|
||||
|
||||
describe("hasFreeBusyConflict", () => {
|
||||
it("returns false for non-vcalendar data", () => {
|
||||
expect(hasFreeBusyConflict({ data: ["not-vcalendar", [], []] })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
describe('hasFreeBusyConflict', () => {
|
||||
it('returns false for non-vcalendar data', () => {
|
||||
expect(hasFreeBusyConflict({ data: ['not-vcalendar', [], []] })).toBe(false)
|
||||
})
|
||||
|
||||
it("returns false when vfreebusy has no freebusy property", () => {
|
||||
it('returns false when vfreebusy has no freebusy property', () => {
|
||||
const data = {
|
||||
data: [
|
||||
"vcalendar",
|
||||
'vcalendar',
|
||||
[],
|
||||
[
|
||||
[
|
||||
"vfreebusy",
|
||||
[["dtstart", {}, "date-time", "2026-03-14T14:00:00Z"]],
|
||||
[],
|
||||
],
|
||||
],
|
||||
],
|
||||
};
|
||||
expect(hasFreeBusyConflict(data)).toBe(false);
|
||||
});
|
||||
'vfreebusy',
|
||||
[['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z']],
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
expect(hasFreeBusyConflict(data)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when vfreebusy contains a freebusy property", () => {
|
||||
it('returns true when vfreebusy contains a freebusy property', () => {
|
||||
const data = {
|
||||
data: [
|
||||
"vcalendar",
|
||||
'vcalendar',
|
||||
[],
|
||||
[
|
||||
[
|
||||
"vfreebusy",
|
||||
'vfreebusy',
|
||||
[
|
||||
["dtstart", {}, "date-time", "2026-03-14T14:00:00Z"],
|
||||
["freebusy", {}, "period", "2026-03-14T14:00:00Z/PT1H"],
|
||||
['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'],
|
||||
['freebusy', {}, 'period', '2026-03-14T14:00:00Z/PT1H']
|
||||
],
|
||||
[],
|
||||
],
|
||||
],
|
||||
],
|
||||
};
|
||||
expect(hasFreeBusyConflict(data)).toBe(true);
|
||||
});
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
expect(hasFreeBusyConflict(data)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for malformed data without throwing", () => {
|
||||
expect(hasFreeBusyConflict(null)).toBe(false);
|
||||
expect(hasFreeBusyConflict("garbage")).toBe(false);
|
||||
expect(hasFreeBusyConflict({ data: null })).toBe(false);
|
||||
});
|
||||
});
|
||||
it('returns false for malformed data without throwing', () => {
|
||||
expect(hasFreeBusyConflict(null)).toBe(false)
|
||||
expect(hasFreeBusyConflict('garbage')).toBe(false)
|
||||
expect(hasFreeBusyConflict({ data: null })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
|
||||
const newAttendee = { email: "alice@example.com", userId: "user-alice" };
|
||||
const newAttendee = { email: 'alice@example.com', userId: 'user-alice' }
|
||||
|
||||
it("shows loading then free for a new attendee", async () => {
|
||||
mockREPORT.mockResolvedValue(false);
|
||||
it('shows loading then free for a new attendee', async () => {
|
||||
mockREPORT.mockResolvedValue(false)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -97,17 +95,17 @@ describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
newAttendees: [newAttendee],
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
expect(result.current[newAttendee.email]).toBe("loading");
|
||||
expect(result.current[newAttendee.email]).toBe('loading')
|
||||
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe("free"));
|
||||
});
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))
|
||||
})
|
||||
|
||||
it("shows busy when REPORT returns true", async () => {
|
||||
mockREPORT.mockResolvedValue(true);
|
||||
it('shows busy when REPORT returns true', async () => {
|
||||
mockREPORT.mockResolvedValue(true)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -115,15 +113,15 @@ describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
newAttendees: [newAttendee],
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe("busy"));
|
||||
});
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe('busy'))
|
||||
})
|
||||
|
||||
it("shows unknown when REPORT throws", async () => {
|
||||
mockREPORT.mockRejectedValue(new Error("network error"));
|
||||
it('shows unknown when REPORT throws', async () => {
|
||||
mockREPORT.mockRejectedValue(new Error('network error'))
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -131,20 +129,20 @@ describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
newAttendees: [newAttendee],
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current[newAttendee.email]).toBe("unknown")
|
||||
);
|
||||
});
|
||||
expect(result.current[newAttendee.email]).toBe('unknown')
|
||||
)
|
||||
})
|
||||
|
||||
it("resolves userId via getUserDataFromEmail when not provided", async () => {
|
||||
mockGetUserData.mockResolvedValue([{ _id: "resolved-id" }] as never);
|
||||
mockREPORT.mockResolvedValue(false);
|
||||
it('resolves userId via getUserDataFromEmail when not provided', async () => {
|
||||
mockGetUserData.mockResolvedValue([{ _id: 'resolved-id' }] as never)
|
||||
mockREPORT.mockResolvedValue(false)
|
||||
|
||||
const attendeeWithoutId = { email: "bob@example.com" };
|
||||
const attendeeWithoutId = { email: 'bob@example.com' }
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -152,42 +150,42 @@ describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
newAttendees: [attendeeWithoutId],
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current[attendeeWithoutId.email]).toBe("free")
|
||||
);
|
||||
expect(mockGetUserData).toHaveBeenCalledWith("bob@example.com");
|
||||
expect(result.current[attendeeWithoutId.email]).toBe('free')
|
||||
)
|
||||
expect(mockGetUserData).toHaveBeenCalledWith('bob@example.com')
|
||||
expect(mockREPORT).toHaveBeenCalledWith(
|
||||
"resolved-id",
|
||||
'resolved-id',
|
||||
expect.any(String),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("shows unknown when userId cannot be resolved", async () => {
|
||||
mockGetUserData.mockResolvedValue([]);
|
||||
it('shows unknown when userId cannot be resolved', async () => {
|
||||
mockGetUserData.mockResolvedValue([])
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
existingAttendees: [],
|
||||
newAttendees: [{ email: "ghost@example.com" }],
|
||||
newAttendees: [{ email: 'ghost@example.com' }],
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current["ghost@example.com"]).toBe("unknown")
|
||||
);
|
||||
expect(mockREPORT).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(result.current['ghost@example.com']).toBe('unknown')
|
||||
)
|
||||
expect(mockREPORT).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does not re-fetch an attendee already fetched", async () => {
|
||||
mockREPORT.mockResolvedValue(false);
|
||||
it('does not re-fetch an attendee already fetched', async () => {
|
||||
mockREPORT.mockResolvedValue(false)
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ attendees }) =>
|
||||
@@ -196,19 +194,19 @@ describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
newAttendees: attendees,
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
}),
|
||||
{ initialProps: { attendees: [newAttendee] } }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe("free"));
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))
|
||||
|
||||
rerender({ attendees: [newAttendee] });
|
||||
expect(mockREPORT).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
rerender({ attendees: [newAttendee] })
|
||||
expect(mockREPORT).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("removes departed attendee from the map", async () => {
|
||||
mockREPORT.mockResolvedValue(false);
|
||||
it('removes departed attendee from the map', async () => {
|
||||
mockREPORT.mockResolvedValue(false)
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ attendees }) =>
|
||||
@@ -217,22 +215,22 @@ describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
newAttendees: attendees,
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
}),
|
||||
{ initialProps: { attendees: [newAttendee] } }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe("free"));
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))
|
||||
|
||||
rerender({ attendees: [] });
|
||||
rerender({ attendees: [] })
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current[newAttendee.email]).toBeUndefined()
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("invalidates cache and re-fetches when time window changes", async () => {
|
||||
mockREPORT.mockResolvedValue(false);
|
||||
it('invalidates cache and re-fetches when time window changes', async () => {
|
||||
mockREPORT.mockResolvedValue(false)
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ start, end }) =>
|
||||
@@ -241,65 +239,65 @@ describe("useAttendeesFreeBusy — Flow B (new attendees)", () => {
|
||||
newAttendees: [newAttendee],
|
||||
start,
|
||||
end,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
}),
|
||||
{ initialProps: { start: START, end: END } }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe("free"));
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))
|
||||
|
||||
rerender({ start: "2026-03-15T14:00:00", end: "2026-03-15T15:00:00" });
|
||||
rerender({ start: '2026-03-15T14:00:00', end: '2026-03-15T15:00:00' })
|
||||
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe("free"));
|
||||
await waitFor(() => expect(result.current[newAttendee.email]).toBe('free'))
|
||||
|
||||
expect(mockREPORT).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(mockREPORT).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("passes UTC-converted iCal times to the API", async () => {
|
||||
mockREPORT.mockResolvedValue(false);
|
||||
it('passes UTC-converted iCal times to the API', async () => {
|
||||
mockREPORT.mockResolvedValue(false)
|
||||
|
||||
renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
existingAttendees: [],
|
||||
newAttendees: [newAttendee],
|
||||
start: "2026-03-14T16:00:00", // 16:00 Paris = 15:00 UTC in winter
|
||||
end: "2026-03-14T17:00:00",
|
||||
timezone: "Europe/Paris",
|
||||
start: '2026-03-14T16:00:00', // 16:00 Paris = 15:00 UTC in winter
|
||||
end: '2026-03-14T17:00:00',
|
||||
timezone: 'Europe/Paris'
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(mockREPORT).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockREPORT).toHaveBeenCalled())
|
||||
|
||||
// Paris is UTC+1 in March (before DST), so 16:00 → 15:00 UTC
|
||||
expect(mockREPORT).toHaveBeenCalledWith(
|
||||
"user-alice",
|
||||
"20260314T150000",
|
||||
"20260314T160000"
|
||||
);
|
||||
});
|
||||
});
|
||||
'user-alice',
|
||||
'20260314T150000',
|
||||
'20260314T160000'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("useAttendeesFreeBusy — Flow A (existing attendees)", () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => {
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
|
||||
const existingAttendee = { email: "carol@example.com", userId: "user-carol" };
|
||||
const existingAttendee = { email: 'carol@example.com', userId: 'user-carol' }
|
||||
|
||||
it("does not fetch when eventUid is missing", () => {
|
||||
it('does not fetch when eventUid is missing', () => {
|
||||
renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
existingAttendees: [existingAttendee],
|
||||
newAttendees: [],
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
timezone: TZ
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
expect(mockPOST).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockPOST).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("shows loading then free for existing attendees", async () => {
|
||||
mockPOST.mockResolvedValue({ "user-carol": false });
|
||||
it('shows loading then free for existing attendees', async () => {
|
||||
mockPOST.mockResolvedValue({ 'user-carol': false })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -308,19 +306,19 @@ describe("useAttendeesFreeBusy — Flow A (existing attendees)", () => {
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
eventUid: "event-123",
|
||||
eventUid: 'event-123'
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
expect(result.current[existingAttendee.email]).toBe("loading");
|
||||
expect(result.current[existingAttendee.email]).toBe('loading')
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current[existingAttendee.email]).toBe("free")
|
||||
);
|
||||
});
|
||||
expect(result.current[existingAttendee.email]).toBe('free')
|
||||
)
|
||||
})
|
||||
|
||||
it("shows busy when POST returns busy for userId", async () => {
|
||||
mockPOST.mockResolvedValue({ "user-carol": true });
|
||||
it('shows busy when POST returns busy for userId', async () => {
|
||||
mockPOST.mockResolvedValue({ 'user-carol': true })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -329,17 +327,17 @@ describe("useAttendeesFreeBusy — Flow A (existing attendees)", () => {
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
eventUid: "event-123",
|
||||
eventUid: 'event-123'
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current[existingAttendee.email]).toBe("busy")
|
||||
);
|
||||
});
|
||||
expect(result.current[existingAttendee.email]).toBe('busy')
|
||||
)
|
||||
})
|
||||
|
||||
it("shows unknown when POST throws", async () => {
|
||||
mockPOST.mockRejectedValue(new Error("server error"));
|
||||
it('shows unknown when POST throws', async () => {
|
||||
mockPOST.mockRejectedValue(new Error('server error'))
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -348,17 +346,17 @@ describe("useAttendeesFreeBusy — Flow A (existing attendees)", () => {
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
eventUid: "event-123",
|
||||
eventUid: 'event-123'
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current[existingAttendee.email]).toBe("unknown")
|
||||
);
|
||||
});
|
||||
expect(result.current[existingAttendee.email]).toBe('unknown')
|
||||
)
|
||||
})
|
||||
|
||||
it("passes the eventUid to the POST API", async () => {
|
||||
mockPOST.mockResolvedValue({ "user-carol": false });
|
||||
it('passes the eventUid to the POST API', async () => {
|
||||
mockPOST.mockResolvedValue({ 'user-carol': false })
|
||||
|
||||
renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
@@ -367,35 +365,35 @@ describe("useAttendeesFreeBusy — Flow A (existing attendees)", () => {
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
eventUid: "event-abc",
|
||||
eventUid: 'event-abc'
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(mockPOST).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockPOST).toHaveBeenCalled())
|
||||
expect(mockPOST).toHaveBeenCalledWith(
|
||||
["user-carol"],
|
||||
['user-carol'],
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
"event-abc"
|
||||
);
|
||||
});
|
||||
});
|
||||
'event-abc'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("useAttendeesFreeBusy — enabled flag", () => {
|
||||
it("does not fetch when disabled", () => {
|
||||
describe('useAttendeesFreeBusy — enabled flag', () => {
|
||||
it('does not fetch when disabled', () => {
|
||||
renderHook(() =>
|
||||
useAttendeesFreeBusy({
|
||||
existingAttendees: [{ email: "x@x.com", userId: "uid" }],
|
||||
newAttendees: [{ email: "y@y.com", userId: "uid2" }],
|
||||
existingAttendees: [{ email: 'x@x.com', userId: 'uid' }],
|
||||
newAttendees: [{ email: 'y@y.com', userId: 'uid2' }],
|
||||
start: START,
|
||||
end: END,
|
||||
timezone: TZ,
|
||||
eventUid: "event-123",
|
||||
enabled: false,
|
||||
eventUid: 'event-123',
|
||||
enabled: false
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
expect(mockPOST).not.toHaveBeenCalled();
|
||||
expect(mockREPORT).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(mockPOST).not.toHaveBeenCalled()
|
||||
expect(mockREPORT).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,67 @@
|
||||
import ImportAlert from "@/features/Events/ImportAlert";
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import ImportAlert from '@/features/Events/ImportAlert'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
// Mock the ErrorSnackbar component since we want to test ImportAlert logic,
|
||||
// but we can also test integration if we don't mock it.
|
||||
// Given ImportAlert uses EventErrorSnackbar which uses MUI Snackbar,
|
||||
// it's better to render it and check for text presence.
|
||||
|
||||
describe("ImportAlert", () => {
|
||||
describe('ImportAlert', () => {
|
||||
const preloadedState = {
|
||||
calendars: {
|
||||
list: {
|
||||
cal1: {
|
||||
id: "cal1",
|
||||
name: "Calendar 1",
|
||||
id: 'cal1',
|
||||
name: 'Calendar 1',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Event 1",
|
||||
error: "Error message 1",
|
||||
uid: 'event1',
|
||||
title: 'Event 1',
|
||||
error: 'Error message 1'
|
||||
},
|
||||
event2: {
|
||||
uid: "event2",
|
||||
title: "Event 2",
|
||||
uid: 'event2',
|
||||
title: 'Event 2'
|
||||
// No error
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
cal2: {
|
||||
id: "cal2",
|
||||
name: "Calendar 2",
|
||||
id: 'cal2',
|
||||
name: 'Calendar 2',
|
||||
events: {
|
||||
event3: {
|
||||
uid: "event3",
|
||||
title: "Event 3",
|
||||
error: "Error message 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
uid: 'event3',
|
||||
title: 'Event 3',
|
||||
error: 'Error message 2'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it("renders nothing when there are no errors", () => {
|
||||
it('renders nothing when there are no errors', () => {
|
||||
const state = {
|
||||
calendars: {
|
||||
list: {
|
||||
cal1: {
|
||||
id: "cal1",
|
||||
id: 'cal1',
|
||||
events: {
|
||||
event1: { uid: "event1" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
event1: { uid: 'event1' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { container } = renderWithProviders(<ImportAlert />, state);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
const { container } = renderWithProviders(<ImportAlert />, state)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it("renders snackbar with error message when there is one error", () => {
|
||||
renderWithProviders(<ImportAlert />, preloadedState);
|
||||
it('renders snackbar with error message when there is one error', () => {
|
||||
renderWithProviders(<ImportAlert />, preloadedState)
|
||||
|
||||
// EventErrorSnackbar shows "multipleEvents" if > 1, or the message if 1.
|
||||
// Here we have 2 errors in preloadedState.
|
||||
@@ -70,24 +70,24 @@ describe("ImportAlert", () => {
|
||||
calendars: {
|
||||
list: {
|
||||
cal1: {
|
||||
id: "cal1",
|
||||
id: 'cal1',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
error: "Single Error",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
uid: 'event1',
|
||||
error: 'Single Error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(<ImportAlert />, singleErrorState);
|
||||
expect(screen.getByText("Single Error")).toBeInTheDocument();
|
||||
});
|
||||
renderWithProviders(<ImportAlert />, singleErrorState)
|
||||
expect(screen.getByText('Single Error')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders summary message when there are multiple errors", () => {
|
||||
renderWithProviders(<ImportAlert />, preloadedState);
|
||||
it('renders summary message when there are multiple errors', () => {
|
||||
renderWithProviders(<ImportAlert />, preloadedState)
|
||||
// 2 errors in preloadedState
|
||||
// "error.multipleEvents" key is used.
|
||||
// In test environment, t function usually returns the key or formatted string.
|
||||
@@ -108,34 +108,34 @@ describe("ImportAlert", () => {
|
||||
// const summary = messages.length === 1 ? messages[0] : t("error.multipleEvents", { count: messages.length });
|
||||
|
||||
// We can check if the Alert is present.
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("dismisses error when close button is clicked", () => {
|
||||
it('dismisses error when close button is clicked', () => {
|
||||
const singleErrorState = {
|
||||
calendars: {
|
||||
list: {
|
||||
cal1: {
|
||||
id: "cal1",
|
||||
id: 'cal1',
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
error: "Dismiss Me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
uid: 'event1',
|
||||
error: 'Dismiss Me'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(<ImportAlert />, singleErrorState);
|
||||
expect(screen.getByText("Dismiss Me")).toBeInTheDocument();
|
||||
renderWithProviders(<ImportAlert />, singleErrorState)
|
||||
expect(screen.getByText('Dismiss Me')).toBeInTheDocument()
|
||||
|
||||
const closeButton = screen.getByRole("button", { name: /ok/i }); // EventErrorSnackbar has an "OK" button
|
||||
fireEvent.click(closeButton);
|
||||
const closeButton = screen.getByRole('button', { name: /ok/i }) // EventErrorSnackbar has an "OK" button
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
// After clicking, it should disappear (or at least be removed from DOM or hidden)
|
||||
// Since we use local state to filter dismissed errors, it should re-render with null.
|
||||
expect(screen.queryByText("Dismiss Me")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByText('Dismiss Me')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+158
-158
@@ -1,213 +1,213 @@
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { updateAttendeesAfterTimeChange } from "@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { userOrganiser } from "@/features/User/userDataTypes";
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import { updateAttendeesAfterTimeChange } from '@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange'
|
||||
import { userAttendee } from '@/features/User/models/attendee'
|
||||
import { userOrganiser } from '@/features/User/userDataTypes'
|
||||
|
||||
describe("updateAttendeesAfterTimeChange", () => {
|
||||
describe('updateAttendeesAfterTimeChange', () => {
|
||||
const mockAttendee: userAttendee = {
|
||||
cal_address: "attendee@example.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cn: "attendee",
|
||||
};
|
||||
cal_address: 'attendee@example.com',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'FALSE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cn: 'attendee'
|
||||
}
|
||||
|
||||
const mockOrganizer: userOrganiser = {
|
||||
cn: "Organizer",
|
||||
cal_address: "organizer@example.com",
|
||||
};
|
||||
cn: 'Organizer',
|
||||
cal_address: 'organizer@example.com'
|
||||
}
|
||||
|
||||
const organizerAttendee: userAttendee = {
|
||||
cal_address: "organizer@example.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
role: "CHAIR",
|
||||
cutype: "INDIVIDUAL",
|
||||
cn: "organizer",
|
||||
};
|
||||
cal_address: 'organizer@example.com',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'FALSE',
|
||||
role: 'CHAIR',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cn: 'organizer'
|
||||
}
|
||||
|
||||
const baseEvent: CalendarEvent = {
|
||||
uid: "test-event",
|
||||
URL: "/calendar/667037022b752d0026472254/cal1/test-event.ics",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: "2025-01-15T07:00:00.000Z",
|
||||
end: "2025-01-15T08:00:00.000Z",
|
||||
uid: 'test-event',
|
||||
URL: '/calendar/667037022b752d0026472254/cal1/test-event.ics',
|
||||
title: 'Test Event',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: '2025-01-15T07:00:00.000Z',
|
||||
end: '2025-01-15T08:00:00.000Z',
|
||||
allday: false,
|
||||
organizer: mockOrganizer,
|
||||
attendee: [mockAttendee, organizerAttendee],
|
||||
timezone: "ETC/UTC",
|
||||
};
|
||||
timezone: 'ETC/UTC'
|
||||
}
|
||||
|
||||
describe("early returns", () => {
|
||||
it("should return the event unchanged when attendee list is undefined", () => {
|
||||
const event = { ...baseEvent, attendee: undefined };
|
||||
describe('early returns', () => {
|
||||
it('should return the event unchanged when attendee list is undefined', () => {
|
||||
const event = { ...baseEvent, attendee: undefined }
|
||||
const result = updateAttendeesAfterTimeChange(
|
||||
event as unknown as CalendarEvent
|
||||
);
|
||||
expect(result).toBe(event);
|
||||
});
|
||||
)
|
||||
expect(result).toBe(event)
|
||||
})
|
||||
|
||||
it("should NOT bail out early when organizer is undefined", () => {
|
||||
const event = { ...baseEvent, organizer: undefined };
|
||||
const result = updateAttendeesAfterTimeChange(event);
|
||||
expect(result.attendee).toBeDefined();
|
||||
expect(result.attendee).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
it('should NOT bail out early when organizer is undefined', () => {
|
||||
const event = { ...baseEvent, organizer: undefined }
|
||||
const result = updateAttendeesAfterTimeChange(event)
|
||||
expect(result.attendee).toBeDefined()
|
||||
expect(result.attendee).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("without provided attendees list", () => {
|
||||
it("should return attendees unchanged when timeChanged is false", () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, false);
|
||||
expect(result.attendee?.[0].partstat).toBe("ACCEPTED");
|
||||
expect(result.attendee?.[0].rsvp).toBe("FALSE");
|
||||
});
|
||||
describe('without provided attendees list', () => {
|
||||
it('should return attendees unchanged when timeChanged is false', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, false)
|
||||
expect(result.attendee?.[0].partstat).toBe('ACCEPTED')
|
||||
expect(result.attendee?.[0].rsvp).toBe('FALSE')
|
||||
})
|
||||
|
||||
it("should mark non-organizer attendees as NEEDS-ACTION when timeChanged is true", () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, true);
|
||||
it('should mark non-organizer attendees as NEEDS-ACTION when timeChanged is true', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, true)
|
||||
const attendee = result.attendee?.find(
|
||||
(a) => a.cal_address === "attendee@example.com"
|
||||
);
|
||||
expect(attendee?.partstat).toBe("NEEDS-ACTION");
|
||||
expect(attendee?.rsvp).toBe("TRUE");
|
||||
});
|
||||
a => a.cal_address === 'attendee@example.com'
|
||||
)
|
||||
expect(attendee?.partstat).toBe('NEEDS-ACTION')
|
||||
expect(attendee?.rsvp).toBe('TRUE')
|
||||
})
|
||||
|
||||
it("should NOT mark the organizer as NEEDS-ACTION when timeChanged is true", () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, true);
|
||||
it('should NOT mark the organizer as NEEDS-ACTION when timeChanged is true', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, true)
|
||||
const organizer = result.attendee?.find(
|
||||
(a) => a.cal_address === "organizer@example.com"
|
||||
);
|
||||
expect(organizer?.partstat).toBe("ACCEPTED");
|
||||
expect(organizer?.rsvp).toBe("FALSE");
|
||||
});
|
||||
a => a.cal_address === 'organizer@example.com'
|
||||
)
|
||||
expect(organizer?.partstat).toBe('ACCEPTED')
|
||||
expect(organizer?.rsvp).toBe('FALSE')
|
||||
})
|
||||
|
||||
it("should return attendees unchanged when timeChanged is undefined", () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent);
|
||||
expect(result.attendee?.[0].partstat).toBe("ACCEPTED");
|
||||
});
|
||||
});
|
||||
it('should return attendees unchanged when timeChanged is undefined', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent)
|
||||
expect(result.attendee?.[0].partstat).toBe('ACCEPTED')
|
||||
})
|
||||
})
|
||||
|
||||
describe("with provided attendees list", () => {
|
||||
describe('with provided attendees list', () => {
|
||||
const newAttendee: userAttendee = {
|
||||
cal_address: "new@example.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cn: "new",
|
||||
};
|
||||
cal_address: 'new@example.com',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'FALSE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cn: 'new'
|
||||
}
|
||||
|
||||
it("should use existing attendee data when the address already exists in the event", () => {
|
||||
it('should use existing attendee data when the address already exists in the event', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
|
||||
mockAttendee,
|
||||
]);
|
||||
mockAttendee
|
||||
])
|
||||
const attendee = result.attendee?.find(
|
||||
(a) => a.cal_address === "attendee@example.com"
|
||||
);
|
||||
expect(attendee?.partstat).toBe("ACCEPTED");
|
||||
});
|
||||
a => a.cal_address === 'attendee@example.com'
|
||||
)
|
||||
expect(attendee?.partstat).toBe('ACCEPTED')
|
||||
})
|
||||
|
||||
it("should mark new attendee as NEEDS-ACTION when not found in existing list", () => {
|
||||
it('should mark new attendee as NEEDS-ACTION when not found in existing list', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
|
||||
newAttendee,
|
||||
]);
|
||||
newAttendee
|
||||
])
|
||||
const attendee = result.attendee?.find(
|
||||
(a) => a.cal_address === "new@example.com"
|
||||
);
|
||||
expect(attendee?.partstat).toBe("NEEDS-ACTION");
|
||||
expect(attendee?.rsvp).toBe("TRUE");
|
||||
});
|
||||
a => a.cal_address === 'new@example.com'
|
||||
)
|
||||
expect(attendee?.partstat).toBe('NEEDS-ACTION')
|
||||
expect(attendee?.rsvp).toBe('TRUE')
|
||||
})
|
||||
|
||||
it("should mark all attendees as NEEDS-ACTION when timeChanged is true", () => {
|
||||
it('should mark all attendees as NEEDS-ACTION when timeChanged is true', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, true, [
|
||||
mockAttendee,
|
||||
newAttendee,
|
||||
]);
|
||||
newAttendee
|
||||
])
|
||||
const nonOrganizerAttendees = result.attendee?.filter(
|
||||
(a) => a.cal_address !== "organizer@example.com"
|
||||
);
|
||||
nonOrganizerAttendees?.forEach((a) => {
|
||||
expect(a.partstat).toBe("NEEDS-ACTION");
|
||||
expect(a.rsvp).toBe("TRUE");
|
||||
});
|
||||
});
|
||||
a => a.cal_address !== 'organizer@example.com'
|
||||
)
|
||||
nonOrganizerAttendees?.forEach(a => {
|
||||
expect(a.partstat).toBe('NEEDS-ACTION')
|
||||
expect(a.rsvp).toBe('TRUE')
|
||||
})
|
||||
})
|
||||
|
||||
it("should append organizer entry at the end of the attendee list", () => {
|
||||
it('should append organizer entry at the end of the attendee list', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
|
||||
mockAttendee,
|
||||
]);
|
||||
const last = result.attendee?.[result.attendee.length - 1];
|
||||
expect(last?.cal_address).toBe("organizer@example.com");
|
||||
});
|
||||
mockAttendee
|
||||
])
|
||||
const last = result.attendee?.[result.attendee.length - 1]
|
||||
expect(last?.cal_address).toBe('organizer@example.com')
|
||||
})
|
||||
|
||||
it("should use existing organizer attendee data when found in event attendees", () => {
|
||||
it('should use existing organizer attendee data when found in event attendees', () => {
|
||||
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
|
||||
mockAttendee,
|
||||
]);
|
||||
mockAttendee
|
||||
])
|
||||
const organizer = result.attendee?.find(
|
||||
(a) => a.cal_address === "organizer@example.com"
|
||||
);
|
||||
expect(organizer?.partstat).toBe("ACCEPTED");
|
||||
});
|
||||
a => a.cal_address === 'organizer@example.com'
|
||||
)
|
||||
expect(organizer?.partstat).toBe('ACCEPTED')
|
||||
})
|
||||
|
||||
it("should fall back to organizer defaults when organizer is not in existing attendees", () => {
|
||||
it('should fall back to organizer defaults when organizer is not in existing attendees', () => {
|
||||
const eventWithoutOrganizerInAttendees: CalendarEvent = {
|
||||
...baseEvent,
|
||||
attendee: [mockAttendee], // organizer not in attendee list
|
||||
};
|
||||
attendee: [mockAttendee] // organizer not in attendee list
|
||||
}
|
||||
const result = updateAttendeesAfterTimeChange(
|
||||
eventWithoutOrganizerInAttendees,
|
||||
false,
|
||||
[mockAttendee]
|
||||
);
|
||||
)
|
||||
const organizer = result.attendee?.find(
|
||||
(a) => a.cal_address === "organizer@example.com"
|
||||
);
|
||||
expect(organizer?.role).toBe("CHAIR");
|
||||
expect(organizer?.partstat).toBe("NEEDS-ACTION");
|
||||
});
|
||||
});
|
||||
a => a.cal_address === 'organizer@example.com'
|
||||
)
|
||||
expect(organizer?.role).toBe('CHAIR')
|
||||
expect(organizer?.partstat).toBe('NEEDS-ACTION')
|
||||
})
|
||||
})
|
||||
|
||||
describe("organizer undefined", () => {
|
||||
describe('organizer undefined', () => {
|
||||
const eventNoOrganizer: CalendarEvent = {
|
||||
...baseEvent,
|
||||
organizer: undefined,
|
||||
};
|
||||
organizer: undefined
|
||||
}
|
||||
|
||||
it("should return attendees without bailing out", () => {
|
||||
const result = updateAttendeesAfterTimeChange(eventNoOrganizer);
|
||||
expect(result.attendee).toHaveLength(2);
|
||||
});
|
||||
it('should return attendees without bailing out', () => {
|
||||
const result = updateAttendeesAfterTimeChange(eventNoOrganizer)
|
||||
expect(result.attendee).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should mark attendees as NEEDS-ACTION when timeChanged is true", () => {
|
||||
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, true);
|
||||
result.attendee?.forEach((a) => {
|
||||
expect(a.partstat).toBe("NEEDS-ACTION");
|
||||
expect(a.rsvp).toBe("TRUE");
|
||||
});
|
||||
});
|
||||
it('should mark attendees as NEEDS-ACTION when timeChanged is true', () => {
|
||||
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, true)
|
||||
result.attendee?.forEach(a => {
|
||||
expect(a.partstat).toBe('NEEDS-ACTION')
|
||||
expect(a.rsvp).toBe('TRUE')
|
||||
})
|
||||
})
|
||||
|
||||
it("should not mutate attendees when timeChanged is false", () => {
|
||||
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, false);
|
||||
expect(result.attendee?.[0].partstat).toBe("ACCEPTED");
|
||||
expect(result.attendee?.[0].rsvp).toBe("FALSE");
|
||||
});
|
||||
it('should not mutate attendees when timeChanged is false', () => {
|
||||
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, false)
|
||||
expect(result.attendee?.[0].partstat).toBe('ACCEPTED')
|
||||
expect(result.attendee?.[0].rsvp).toBe('FALSE')
|
||||
})
|
||||
|
||||
it("should not append an organizer entry when organizer is undefined and attendees list is provided", () => {
|
||||
it('should not append an organizer entry when organizer is undefined and attendees list is provided', () => {
|
||||
const newAttendee: userAttendee = {
|
||||
cal_address: "new@example.com",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cn: "new",
|
||||
};
|
||||
cal_address: 'new@example.com',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'FALSE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cn: 'new'
|
||||
}
|
||||
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, false, [
|
||||
newAttendee,
|
||||
]);
|
||||
expect(result.attendee).toHaveLength(1);
|
||||
expect(result.attendee?.[0].cal_address).toBe("new@example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
newAttendee
|
||||
])
|
||||
expect(result.attendee).toHaveLength(1)
|
||||
expect(result.attendee?.[0].cal_address).toBe('new@example.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user