* #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:
+5
-5
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
|
||||
@@ -103,21 +103,21 @@ Example:
|
||||
```js
|
||||
var appList = [
|
||||
{
|
||||
name: "Chat",
|
||||
link: "/twake",
|
||||
icon: "/assets/images/svg/app-chat.svg",
|
||||
name: 'Chat',
|
||||
link: '/twake',
|
||||
icon: '/assets/images/svg/app-chat.svg'
|
||||
},
|
||||
{
|
||||
name: "Drive",
|
||||
link: "/drive",
|
||||
icon: "/assets/images/svg/app-drive.svg",
|
||||
name: 'Drive',
|
||||
link: '/drive',
|
||||
icon: '/assets/images/svg/app-drive.svg'
|
||||
},
|
||||
{
|
||||
name: "Mail",
|
||||
link: "/mail",
|
||||
icon: "/assets/images/svg/app-mail.svg",
|
||||
},
|
||||
];
|
||||
name: 'Mail',
|
||||
link: '/mail',
|
||||
icon: '/assets/images/svg/app-mail.svg'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Note**: `appList.js` is gitignored, so each environment can have its own configuration. The icon files in `public/assets/images/svg/` should be committed to the repository.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,106 +1,106 @@
|
||||
import CalendarLayout from "@/components/Calendar/CalendarLayout";
|
||||
import { act, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import CalendarLayout from '@/components/Calendar/CalendarLayout'
|
||||
import { act, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
const PIVOT_UTC = new Date("2026-03-16T17:00:00Z");
|
||||
const PIVOT_UTC = new Date('2026-03-16T17:00:00Z')
|
||||
|
||||
const makeState = (timezone: string) => ({
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
tokens: { accessToken: "token" },
|
||||
tokens: { accessToken: 'token' }
|
||||
},
|
||||
settings: { view: "calendar", timeZone: timezone },
|
||||
settings: { view: 'calendar', timeZone: timezone },
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "Calendar personal",
|
||||
id: "user1/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["test@test.com"] },
|
||||
events: {},
|
||||
},
|
||||
'user1/cal1': {
|
||||
name: 'Calendar personal',
|
||||
id: 'user1/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['test@test.com'] },
|
||||
events: {}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
});
|
||||
pending: false
|
||||
}
|
||||
})
|
||||
|
||||
describe("Calendar – dayHeaderContent respects selected timezone", () => {
|
||||
describe('Calendar – dayHeaderContent respects selected timezone', () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
async function renderAtPivotTime(timezone: string): Promise<string[]> {
|
||||
// Set fake timers the same way the working test does — single chained call
|
||||
jest.useFakeTimers().setSystemTime(PIVOT_UTC);
|
||||
jest.useFakeTimers().setSystemTime(PIVOT_UTC)
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(<CalendarLayout />, makeState(timezone));
|
||||
});
|
||||
renderWithProviders(<CalendarLayout />, makeState(timezone))
|
||||
})
|
||||
|
||||
// Advance past the debounce (300ms) like the working navigation test does
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
const calendarRef = window.__calendarRef;
|
||||
await waitFor(() => expect(calendarRef?.current).not.toBeNull());
|
||||
const calendarRef = window.__calendarRef
|
||||
await waitFor(() => expect(calendarRef?.current).not.toBeNull())
|
||||
|
||||
await act(async () => {
|
||||
calendarRef.current?.changeView("timeGridWeek");
|
||||
});
|
||||
calendarRef.current?.changeView('timeGridWeek')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelectorAll(".fc-col-header-cell").length).toBe(7);
|
||||
});
|
||||
expect(document.querySelectorAll('.fc-col-header-cell').length).toBe(7)
|
||||
})
|
||||
|
||||
return Array.from(document.querySelectorAll(".fc-col-header-cell")).map(
|
||||
(cell) => cell.textContent ?? ""
|
||||
);
|
||||
return Array.from(document.querySelectorAll('.fc-col-header-cell')).map(
|
||||
cell => cell.textContent ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
it("UTC: Monday March 16 column exists", async () => {
|
||||
const cells = await renderAtPivotTime("UTC");
|
||||
const monday = cells.find((t) => t.includes("16"));
|
||||
expect(monday).toBeDefined();
|
||||
expect(monday).toMatch(/MON/i);
|
||||
});
|
||||
it('UTC: Monday March 16 column exists', async () => {
|
||||
const cells = await renderAtPivotTime('UTC')
|
||||
const monday = cells.find(t => t.includes('16'))
|
||||
expect(monday).toBeDefined()
|
||||
expect(monday).toMatch(/MON/i)
|
||||
})
|
||||
|
||||
it("Europe/Paris: Monday March 16 column exists (UTC+1, no day shift)", async () => {
|
||||
const cells = await renderAtPivotTime("Europe/Paris");
|
||||
const monday = cells.find((t) => t.includes("16"));
|
||||
expect(monday).toBeDefined();
|
||||
expect(monday).toMatch(/MON/i);
|
||||
});
|
||||
it('Europe/Paris: Monday March 16 column exists (UTC+1, no day shift)', async () => {
|
||||
const cells = await renderAtPivotTime('Europe/Paris')
|
||||
const monday = cells.find(t => t.includes('16'))
|
||||
expect(monday).toBeDefined()
|
||||
expect(monday).toMatch(/MON/i)
|
||||
})
|
||||
|
||||
it("Asia/Jakarta: Tuesday March 17 column exists (UTC+7, day shifts forward)", async () => {
|
||||
const cells = await renderAtPivotTime("Asia/Jakarta");
|
||||
const tuesday = cells.find((t) => t.includes("17"));
|
||||
expect(tuesday).toBeDefined();
|
||||
expect(tuesday).toMatch(/TUE/i);
|
||||
});
|
||||
it('Asia/Jakarta: Tuesday March 17 column exists (UTC+7, day shifts forward)', async () => {
|
||||
const cells = await renderAtPivotTime('Asia/Jakarta')
|
||||
const tuesday = cells.find(t => t.includes('17'))
|
||||
expect(tuesday).toBeDefined()
|
||||
expect(tuesday).toMatch(/TUE/i)
|
||||
})
|
||||
|
||||
it("Asia/Jakarta: no column incorrectly labeled MON 17 – regression", async () => {
|
||||
const cells = await renderAtPivotTime("Asia/Jakarta");
|
||||
const wrongCell = cells.find((t) => t.includes("17") && /MON/i.test(t));
|
||||
expect(wrongCell).toBeUndefined();
|
||||
});
|
||||
it('Asia/Jakarta: no column incorrectly labeled MON 17 – regression', async () => {
|
||||
const cells = await renderAtPivotTime('Asia/Jakarta')
|
||||
const wrongCell = cells.find(t => t.includes('17') && /MON/i.test(t))
|
||||
expect(wrongCell).toBeUndefined()
|
||||
})
|
||||
|
||||
it("Asia/Tokyo: Tuesday March 17 column exists (UTC+9)", async () => {
|
||||
const cells = await renderAtPivotTime("Asia/Tokyo");
|
||||
const tuesday = cells.find((t) => t.includes("17"));
|
||||
expect(tuesday).toBeDefined();
|
||||
expect(tuesday).toMatch(/TUE/i);
|
||||
});
|
||||
it('Asia/Tokyo: Tuesday March 17 column exists (UTC+9)', async () => {
|
||||
const cells = await renderAtPivotTime('Asia/Tokyo')
|
||||
const tuesday = cells.find(t => t.includes('17'))
|
||||
expect(tuesday).toBeDefined()
|
||||
expect(tuesday).toMatch(/TUE/i)
|
||||
})
|
||||
|
||||
it("America/New_York: Monday March 16 column exists (UTC-4, no day shift)", async () => {
|
||||
const cells = await renderAtPivotTime("America/New_York");
|
||||
const monday = cells.find((t) => t.includes("16"));
|
||||
expect(monday).toBeDefined();
|
||||
expect(monday).toMatch(/MON/i);
|
||||
});
|
||||
});
|
||||
it('America/New_York: Monday March 16 column exists (UTC-4, no day shift)', async () => {
|
||||
const cells = await renderAtPivotTime('America/New_York')
|
||||
const monday = cells.find(t => t.includes('16'))
|
||||
expect(monday).toBeDefined()
|
||||
expect(monday).toMatch(/MON/i)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,413 +1,411 @@
|
||||
import CalendarSearch from "@/components/Calendar/CalendarSearch";
|
||||
import * as CalendarApi from "@/features/Calendars/CalendarApi";
|
||||
import * as CalendarSlice from "@/features/Calendars/services";
|
||||
import { searchUsers } from "@/features/User/userAPI";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import CalendarSearch from '@/components/Calendar/CalendarSearch'
|
||||
import * as CalendarApi from '@/features/Calendars/CalendarApi'
|
||||
import * as CalendarSlice from '@/features/Calendars/services'
|
||||
import { searchUsers } from '@/features/User/userAPI'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/features/User/userAPI");
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock('@/features/User/userAPI')
|
||||
jest.mock('@/features/Calendars/CalendarApi')
|
||||
|
||||
const mockedSearchUsers = searchUsers as jest.MockedFunction<
|
||||
typeof searchUsers
|
||||
>;
|
||||
const mockedSearchUsers = searchUsers as jest.MockedFunction<typeof searchUsers>
|
||||
const mockedGetCalendars = CalendarApi.getCalendars as jest.MockedFunction<
|
||||
typeof CalendarApi.getCalendars
|
||||
>;
|
||||
>
|
||||
|
||||
describe("CalendarSearch", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('CalendarSearch', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
const mockUser = {
|
||||
email: "user@example.com",
|
||||
displayName: "Test User",
|
||||
avatarUrl: "https://example.com/avatar.jpg",
|
||||
openpaasId: "user123",
|
||||
};
|
||||
email: 'user@example.com',
|
||||
displayName: 'Test User',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
openpaasId: 'user123'
|
||||
}
|
||||
|
||||
const mockCalendar = {
|
||||
"dav:name": "Test Calendar",
|
||||
"apple:color": "#FF0000",
|
||||
'dav:name': 'Test Calendar',
|
||||
'apple:color': '#FF0000',
|
||||
_links: {
|
||||
self: {
|
||||
href: "/calendars/user123/cal1.json",
|
||||
},
|
||||
},
|
||||
};
|
||||
href: '/calendars/user123/cal1.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
tokens: { accessToken: "token" },
|
||||
tokens: { accessToken: 'token' }
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "My Calendar",
|
||||
id: "user1/cal1",
|
||||
color: "#0000FF",
|
||||
owner: { emails: ["test@test.com"] },
|
||||
events: {},
|
||||
},
|
||||
'user1/cal1': {
|
||||
name: 'My Calendar',
|
||||
id: 'user1/cal1',
|
||||
color: '#0000FF',
|
||||
owner: { emails: ['test@test.com'] },
|
||||
events: {}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("searches for users and displays their calendars", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser]);
|
||||
it('searches for users and displays their calendars', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser])
|
||||
mockedGetCalendars.mockResolvedValueOnce({
|
||||
_embedded: {
|
||||
"dav:calendar": [mockCalendar],
|
||||
},
|
||||
});
|
||||
'dav:calendar': [mockCalendar]
|
||||
}
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
const option = await screen.findByText('Test User')
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
fireEvent.click(option)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith(
|
||||
"user123",
|
||||
"sharedPublic=true&"
|
||||
);
|
||||
});
|
||||
'user123',
|
||||
'sharedPublic=true&'
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("user@example.com")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument()
|
||||
expect(screen.getByText('user@example.com')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("adds selected calendars on save", async () => {
|
||||
it('adds selected calendars on save', async () => {
|
||||
const addSharedCalendarSpy = jest
|
||||
.spyOn(CalendarSlice, "addSharedCalendarAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
.spyOn(CalendarSlice, 'addSharedCalendarAsync')
|
||||
.mockImplementation(payload => {
|
||||
return () => Promise.resolve(payload) as any
|
||||
})
|
||||
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser]);
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser])
|
||||
mockedGetCalendars.mockResolvedValueOnce({
|
||||
_embedded: {
|
||||
"dav:calendar": [mockCalendar],
|
||||
},
|
||||
});
|
||||
'dav:calendar': [mockCalendar]
|
||||
}
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
const option = await screen.findByText('Test User')
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
fireEvent.click(option)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
const addButton = screen.getByRole('button', { name: /add/i })
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
fireEvent.click(addButton)
|
||||
})
|
||||
|
||||
expect(addSharedCalendarSpy).toHaveBeenCalled();
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
expect(addSharedCalendarSpy).toHaveBeenCalled()
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does not add calendars that already exist", async () => {
|
||||
it('does not add calendars that already exist', async () => {
|
||||
const addSharedCalendarSpy = jest
|
||||
.spyOn(CalendarSlice, "addSharedCalendarAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
.spyOn(CalendarSlice, 'addSharedCalendarAsync')
|
||||
.mockImplementation(payload => {
|
||||
return () => Promise.resolve(payload) as any
|
||||
})
|
||||
|
||||
const existingCalendar = {
|
||||
...mockCalendar,
|
||||
_links: {
|
||||
self: {
|
||||
href: "/calendars/user1/cal1.json",
|
||||
},
|
||||
},
|
||||
};
|
||||
href: '/calendars/user1/cal1.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser]);
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser])
|
||||
mockedGetCalendars.mockResolvedValueOnce({
|
||||
_embedded: {
|
||||
"dav:calendar": [existingCalendar],
|
||||
},
|
||||
});
|
||||
'dav:calendar': [existingCalendar]
|
||||
}
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
const option = await screen.findByText('Test User')
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
fireEvent.click(option)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("calendar.noMoreCalendarsFor(name=Test User)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getByText('calendar.noMoreCalendarsFor(name=Test User)')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
const addButton = screen.getByRole('button', { name: /add/i })
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
fireEvent.click(addButton)
|
||||
})
|
||||
|
||||
expect(addSharedCalendarSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(addSharedCalendarSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("displays message when user has no publicly available calendars", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser]);
|
||||
mockedGetCalendars.mockResolvedValueOnce({});
|
||||
it('displays message when user has no publicly available calendars', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser])
|
||||
mockedGetCalendars.mockResolvedValueOnce({})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
const option = await screen.findByText('Test User')
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
fireEvent.click(option)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("calendar.noPublicCalendarsFor(name=Test User)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
screen.getByText('calendar.noPublicCalendarsFor(name=Test User)')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("changes calendar color", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser]);
|
||||
it('changes calendar color', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser])
|
||||
mockedGetCalendars.mockResolvedValueOnce({
|
||||
_embedded: {
|
||||
"dav:calendar": [mockCalendar],
|
||||
},
|
||||
});
|
||||
'dav:calendar': [mockCalendar]
|
||||
}
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
const option = await screen.findByText('Test User')
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
fireEvent.click(option)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ColorPicker would need to be interacted with based on its implementation
|
||||
// This is a placeholder for color change interaction
|
||||
const colorPicker = document.querySelector('[data-testid="color-picker"]');
|
||||
const colorPicker = document.querySelector('[data-testid="color-picker"]')
|
||||
if (colorPicker) {
|
||||
await act(async () => {
|
||||
fireEvent.click(colorPicker);
|
||||
});
|
||||
fireEvent.click(colorPicker)
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it("handles multiple calendars from the same user", async () => {
|
||||
it('handles multiple calendars from the same user', async () => {
|
||||
const secondCalendar = {
|
||||
"dav:name": "Second Calendar",
|
||||
"apple:color": "#00FF00",
|
||||
'dav:name': 'Second Calendar',
|
||||
'apple:color': '#00FF00',
|
||||
_links: {
|
||||
self: {
|
||||
href: "/calendars/user123/cal2.json",
|
||||
},
|
||||
},
|
||||
};
|
||||
href: '/calendars/user123/cal2.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser]);
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser])
|
||||
mockedGetCalendars.mockResolvedValueOnce({
|
||||
_embedded: {
|
||||
"dav:calendar": [mockCalendar, secondCalendar],
|
||||
},
|
||||
});
|
||||
'dav:calendar': [mockCalendar, secondCalendar]
|
||||
}
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
const option = await screen.findByText('Test User')
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
fireEvent.click(option)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Second Calendar/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Second Calendar/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("does not call addSharedCalendarAsync when no calendars are selected", async () => {
|
||||
it('does not call addSharedCalendarAsync when no calendars are selected', async () => {
|
||||
const addSharedCalendarSpy = jest
|
||||
.spyOn(CalendarSlice, "addSharedCalendarAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
.spyOn(CalendarSlice, 'addSharedCalendarAsync')
|
||||
.mockImplementation(payload => {
|
||||
return () => Promise.resolve(payload) as any
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
const addButton = screen.getByRole('button', { name: /add/i })
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
fireEvent.click(addButton)
|
||||
})
|
||||
|
||||
expect(addSharedCalendarSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it("BUGFIX : handles calendar with no apple:color", async () => {
|
||||
expect(addSharedCalendarSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
it('BUGFIX : handles calendar with no apple:color', async () => {
|
||||
const addSharedCalendarSpy = jest
|
||||
.spyOn(CalendarSlice, "addSharedCalendarAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
.spyOn(CalendarSlice, 'addSharedCalendarAsync')
|
||||
.mockImplementation(payload => {
|
||||
return () => Promise.resolve(payload) as any
|
||||
})
|
||||
const mockCalendarNoColor = {
|
||||
"dav:name": "Test Calendar",
|
||||
'dav:name': 'Test Calendar',
|
||||
_links: {
|
||||
self: {
|
||||
href: "/calendars/user123/cal2.json",
|
||||
},
|
||||
},
|
||||
};
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser]);
|
||||
href: '/calendars/user123/cal2.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
mockedSearchUsers.mockResolvedValueOnce([mockUser])
|
||||
mockedGetCalendars.mockResolvedValueOnce({
|
||||
_embedded: {
|
||||
"dav:calendar": [mockCalendarNoColor],
|
||||
},
|
||||
});
|
||||
'dav:calendar': [mockCalendarNoColor]
|
||||
}
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSearch open={true} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", expect.anything());
|
||||
});
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith('Test', expect.anything())
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
const option = await screen.findByText('Test User')
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
fireEvent.click(option)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith(
|
||||
"user123",
|
||||
"sharedPublic=true&"
|
||||
);
|
||||
});
|
||||
'user123',
|
||||
'sharedPublic=true&'
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("user@example.com")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/Test Calendar/i)).toBeInTheDocument()
|
||||
expect(screen.getByText('user@example.com')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add/i });
|
||||
const addButton = screen.getByRole('button', { name: /add/i })
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
fireEvent.click(addButton)
|
||||
})
|
||||
await waitFor(() =>
|
||||
expect(addSharedCalendarSpy).toHaveBeenCalledWith({
|
||||
cal: {
|
||||
cal: {
|
||||
_links: { self: { href: "/calendars/user123/cal2.json" } },
|
||||
"dav:name": "Test Calendar",
|
||||
_links: { self: { href: '/calendars/user123/cal2.json' } },
|
||||
'dav:name': 'Test Calendar'
|
||||
},
|
||||
color: { dark: "#329655", light: "#D0ECDA" },
|
||||
color: { dark: '#329655', light: '#D0ECDA' },
|
||||
owner: {
|
||||
avatarUrl: "https://example.com/avatar.jpg",
|
||||
displayName: "Test User",
|
||||
email: "user@example.com",
|
||||
openpaasId: "user123",
|
||||
},
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
displayName: 'Test User',
|
||||
email: 'user@example.com',
|
||||
openpaasId: 'user123'
|
||||
}
|
||||
},
|
||||
calId: expect.any(String),
|
||||
userId: "user1",
|
||||
userId: 'user1'
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledWith(
|
||||
expect.arrayContaining(["user123/cal2"])
|
||||
);
|
||||
});
|
||||
});
|
||||
expect.arrayContaining(['user123/cal2'])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,188 +1,188 @@
|
||||
import CalendarResources from "@/components/Calendar/CalendarResources";
|
||||
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
||||
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import CalendarResources from '@/components/Calendar/CalendarResources'
|
||||
import { getCalendars } from '@/features/Calendars/CalendarApi'
|
||||
import { addCalendarResourceAsync } from '@/features/Calendars/api/addCalendarResourceAsync'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock("@/features/Calendars/api/addCalendarResourceAsync");
|
||||
jest.mock("@/components/Attendees/ResourceSearch", () => ({
|
||||
jest.mock('@/features/Calendars/CalendarApi')
|
||||
jest.mock('@/features/Calendars/api/addCalendarResourceAsync')
|
||||
jest.mock('@/components/Attendees/ResourceSearch', () => ({
|
||||
ResourceSearch: ({
|
||||
onChange,
|
||||
onChange
|
||||
}: {
|
||||
onChange: (
|
||||
event: null,
|
||||
value: { displayName: string; openpaasId: string }[]
|
||||
) => void;
|
||||
) => void
|
||||
}) => (
|
||||
<div data-testid="resource-search">
|
||||
<button
|
||||
data-testid="mock-resource-search-select"
|
||||
onClick={() =>
|
||||
onChange(null, [
|
||||
{ displayName: "Room A", openpaasId: "room-a-id" },
|
||||
{ displayName: "Room B", openpaasId: "room-b-id" },
|
||||
{ displayName: 'Room A', openpaasId: 'room-a-id' },
|
||||
{ displayName: 'Room B', openpaasId: 'room-b-id' }
|
||||
])
|
||||
}
|
||||
>
|
||||
Select Resources
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
)
|
||||
}))
|
||||
|
||||
const mockedGetCalendars = getCalendars as jest.Mock;
|
||||
const mockedGetCalendars = getCalendars as jest.Mock
|
||||
const mockedAddCalendarResourceAsync =
|
||||
addCalendarResourceAsync as unknown as jest.Mock;
|
||||
addCalendarResourceAsync as unknown as jest.Mock
|
||||
|
||||
describe("CalendarResources", () => {
|
||||
describe('CalendarResources', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const baseUser = {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
tokens: { accessToken: "token" },
|
||||
};
|
||||
tokens: { accessToken: 'token' }
|
||||
}
|
||||
|
||||
const setup = (isOpen = true) => {
|
||||
const onClose = jest.fn();
|
||||
const onClose = jest.fn()
|
||||
renderWithProviders(<CalendarResources onClose={onClose} open={isOpen} />, {
|
||||
user: baseUser,
|
||||
});
|
||||
return { onClose };
|
||||
};
|
||||
user: baseUser
|
||||
})
|
||||
return { onClose }
|
||||
}
|
||||
|
||||
it("renders correctly and closes on cancel", () => {
|
||||
const { onClose } = setup();
|
||||
it('renders correctly and closes on cancel', () => {
|
||||
const { onClose } = setup()
|
||||
|
||||
expect(screen.getByText("calendar.browseResources")).toBeInTheDocument();
|
||||
expect(screen.getByText('calendar.browseResources')).toBeInTheDocument()
|
||||
|
||||
const cancelButton = screen.getByRole("button", {
|
||||
name: "common.cancel",
|
||||
});
|
||||
fireEvent.click(cancelButton);
|
||||
const cancelButton = screen.getByRole('button', {
|
||||
name: 'common.cancel'
|
||||
})
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does not render when isOpen is false", () => {
|
||||
setup(false);
|
||||
it('does not render when isOpen is false', () => {
|
||||
setup(false)
|
||||
expect(
|
||||
screen.queryByText("calendar.browseResources")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
screen.queryByText('calendar.browseResources')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("fetches resource calendars and adds them using Promise.allSettled logic", async () => {
|
||||
it('fetches resource calendars and adds them using Promise.allSettled logic', async () => {
|
||||
const mockCalendarDataA = {
|
||||
_embedded: {
|
||||
"dav:calendar": [
|
||||
'dav:calendar': [
|
||||
{
|
||||
_links: { self: { href: "/calendars/room-a-id/cal-a.json" } },
|
||||
"dav:name": "Room A Calendar",
|
||||
"caldav:description": "Main room A calendar",
|
||||
color: "red",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
_links: { self: { href: '/calendars/room-a-id/cal-a.json' } },
|
||||
'dav:name': 'Room A Calendar',
|
||||
'caldav:description': 'Main room A calendar',
|
||||
color: 'red'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
mockedGetCalendars.mockImplementation((userId) => {
|
||||
if (userId === "room-a-id") return Promise.resolve(mockCalendarDataA);
|
||||
mockedGetCalendars.mockImplementation(userId => {
|
||||
if (userId === 'room-a-id') return Promise.resolve(mockCalendarDataA)
|
||||
// Simulate failure for room-b
|
||||
if (userId === "room-b-id")
|
||||
return Promise.reject(new Error("Failed fetching Room B"));
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
if (userId === 'room-b-id')
|
||||
return Promise.reject(new Error('Failed fetching Room B'))
|
||||
return Promise.resolve([])
|
||||
})
|
||||
|
||||
const mockDispatchResult = {
|
||||
unwrap: () => Promise.resolve({ type: "success" }),
|
||||
};
|
||||
mockedAddCalendarResourceAsync.mockReturnValue(() => mockDispatchResult);
|
||||
unwrap: () => Promise.resolve({ type: 'success' })
|
||||
}
|
||||
mockedAddCalendarResourceAsync.mockReturnValue(() => mockDispatchResult)
|
||||
|
||||
const { onClose } = setup();
|
||||
const { onClose } = setup()
|
||||
|
||||
// Trigger resource selection
|
||||
const mockSelectBtn = screen.getByTestId("mock-resource-search-select");
|
||||
const mockSelectBtn = screen.getByTestId('mock-resource-search-select')
|
||||
await act(async () => {
|
||||
fireEvent.click(mockSelectBtn);
|
||||
});
|
||||
fireEvent.click(mockSelectBtn)
|
||||
})
|
||||
|
||||
// After selection, the button should be enabled and say Add
|
||||
const addButton = await screen.findByRole("button", {
|
||||
name: "actions.add",
|
||||
});
|
||||
expect(addButton).not.toBeDisabled();
|
||||
const addButton = await screen.findByRole('button', {
|
||||
name: 'actions.add'
|
||||
})
|
||||
expect(addButton).not.toBeDisabled()
|
||||
|
||||
// Click add to trigger addCalendarResourceAsync
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
fireEvent.click(addButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
// It should only have been called for Room A because Room B failed (Promise.allSettled)
|
||||
expect(mockedAddCalendarResourceAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockedAddCalendarResourceAsync).toHaveBeenCalledTimes(1)
|
||||
|
||||
const payload = mockedAddCalendarResourceAsync.mock.calls[0][0];
|
||||
const payload = mockedAddCalendarResourceAsync.mock.calls[0][0]
|
||||
expect(payload).toEqual(
|
||||
expect.objectContaining({
|
||||
userId: "user1",
|
||||
userId: 'user1',
|
||||
calId: expect.any(String),
|
||||
cal: expect.objectContaining({
|
||||
cal: mockCalendarDataA._embedded["dav:calendar"][0],
|
||||
}),
|
||||
cal: mockCalendarDataA._embedded['dav:calendar'][0]
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("adds existing user details fallback logic within submit handler", async () => {
|
||||
it('adds existing user details fallback logic within submit handler', async () => {
|
||||
const mockCalendarDataA = {
|
||||
_embedded: {
|
||||
"dav:calendar": [
|
||||
'dav:calendar': [
|
||||
{
|
||||
_links: { self: { href: "/calendars/room-a-id/cal-a.json" } },
|
||||
"dav:name": "Room A Calendar",
|
||||
"caldav:description": "Main room A calendar",
|
||||
color: "red",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
_links: { self: { href: '/calendars/room-a-id/cal-a.json' } },
|
||||
'dav:name': 'Room A Calendar',
|
||||
'caldav:description': 'Main room A calendar',
|
||||
color: 'red'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
mockedGetCalendars.mockResolvedValueOnce(mockCalendarDataA);
|
||||
mockedGetCalendars.mockResolvedValueOnce([]); // Mock array of size 2 (for both A and B but second resolves empty)
|
||||
mockedGetCalendars.mockResolvedValueOnce(mockCalendarDataA)
|
||||
mockedGetCalendars.mockResolvedValueOnce([]) // Mock array of size 2 (for both A and B but second resolves empty)
|
||||
|
||||
// Simulate successful API response
|
||||
const mockDispatchResult = {
|
||||
unwrap: () => Promise.resolve(),
|
||||
};
|
||||
mockedAddCalendarResourceAsync.mockReturnValue(() => mockDispatchResult);
|
||||
unwrap: () => Promise.resolve()
|
||||
}
|
||||
mockedAddCalendarResourceAsync.mockReturnValue(() => mockDispatchResult)
|
||||
|
||||
const { onClose } = setup();
|
||||
const { onClose } = setup()
|
||||
|
||||
// Trigger resource selection
|
||||
const mockSelectBtn = screen.getByTestId("mock-resource-search-select");
|
||||
const mockSelectBtn = screen.getByTestId('mock-resource-search-select')
|
||||
await act(async () => {
|
||||
fireEvent.click(mockSelectBtn);
|
||||
});
|
||||
fireEvent.click(mockSelectBtn)
|
||||
})
|
||||
|
||||
const addButton = await screen.findByRole("button", {
|
||||
name: "actions.add",
|
||||
});
|
||||
const addButton = await screen.findByRole('button', {
|
||||
name: 'actions.add'
|
||||
})
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
fireEvent.click(addButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,80 +1,80 @@
|
||||
import CalendarSelection from "@/components/Calendar/CalendarSelection";
|
||||
import * as calendarThunks from "@/features/Calendars/services";
|
||||
import "@testing-library/jest-dom";
|
||||
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import CalendarSelection from '@/components/Calendar/CalendarSelection'
|
||||
import * as calendarThunks from '@/features/Calendars/services'
|
||||
import '@testing-library/jest-dom'
|
||||
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
describe("CalendarSelection", () => {
|
||||
describe('CalendarSelection', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
localStorage.clear()
|
||||
})
|
||||
const baseUser = {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
tokens: { accessToken: "token" },
|
||||
};
|
||||
tokens: { accessToken: 'token' }
|
||||
}
|
||||
|
||||
const calendarsMock = {
|
||||
"user1/cal1": {
|
||||
name: "Calendar personal",
|
||||
id: "user1/cal1",
|
||||
color: "#FF0000",
|
||||
owner: { emails: ["alice@example.com"], lastname: "alice" },
|
||||
'user1/cal1': {
|
||||
name: 'Calendar personal',
|
||||
id: 'user1/cal1',
|
||||
color: '#FF0000',
|
||||
owner: { emails: ['alice@example.com'], lastname: 'alice' }
|
||||
},
|
||||
"user2/cal1": {
|
||||
name: "Calendar delegated",
|
||||
'user2/cal1': {
|
||||
name: 'Calendar delegated',
|
||||
delegated: true,
|
||||
id: "user2/cal1",
|
||||
color: "#00FF00",
|
||||
id: 'user2/cal1',
|
||||
color: '#00FF00',
|
||||
owner: {
|
||||
firstname: "Bob",
|
||||
lastname: "Builder",
|
||||
emails: ["bob@example.com"],
|
||||
},
|
||||
firstname: 'Bob',
|
||||
lastname: 'Builder',
|
||||
emails: ['bob@example.com']
|
||||
}
|
||||
},
|
||||
"user3/cal1": {
|
||||
name: "Calendar shared",
|
||||
id: "user3/cal1",
|
||||
color: "#0000FF",
|
||||
'user3/cal1': {
|
||||
name: 'Calendar shared',
|
||||
id: 'user3/cal1',
|
||||
color: '#0000FF',
|
||||
owner: {
|
||||
firstname: "Charlie",
|
||||
lastname: "Chaplin",
|
||||
emails: ["charlie@example.com"],
|
||||
},
|
||||
},
|
||||
};
|
||||
firstname: 'Charlie',
|
||||
lastname: 'Chaplin',
|
||||
emails: ['charlie@example.com']
|
||||
}
|
||||
}
|
||||
}
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
it("renders personal, delegated and calendar.other", () => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
it('renders personal, delegated and calendar.other', () => {
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={["user1/cal1"]}
|
||||
selectedCalendars={['user1/cal1']}
|
||||
setSelectedCalendars={jest.fn()}
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("calendar.personal")).toBeInTheDocument();
|
||||
expect(screen.getByText("calendar.delegated")).toBeInTheDocument();
|
||||
expect(screen.getByText("calendar.other")).toBeInTheDocument();
|
||||
expect(screen.getByText('calendar.personal')).toBeInTheDocument()
|
||||
expect(screen.getByText('calendar.delegated')).toBeInTheDocument()
|
||||
expect(screen.getByText('calendar.other')).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByLabelText("Calendar personal")).toBeChecked();
|
||||
expect(screen.getByLabelText("Calendar delegated")).not.toBeChecked();
|
||||
expect(screen.getByLabelText("Calendar shared")).not.toBeChecked();
|
||||
});
|
||||
expect(screen.getByLabelText('Calendar personal')).toBeChecked()
|
||||
expect(screen.getByLabelText('Calendar delegated')).not.toBeChecked()
|
||||
expect(screen.getByLabelText('Calendar shared')).not.toBeChecked()
|
||||
})
|
||||
|
||||
it("toggles a calendar selection on click", () => {
|
||||
const setSelectedCalendars = jest.fn();
|
||||
it('toggles a calendar selection on click', () => {
|
||||
const setSelectedCalendars = jest.fn()
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
@@ -83,41 +83,41 @@ describe("CalendarSelection", () => {
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const checkbox = screen.getByLabelText("Calendar personal");
|
||||
fireEvent.click(checkbox);
|
||||
const checkbox = screen.getByLabelText('Calendar personal')
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
expect(setSelectedCalendars).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(setSelectedCalendars).toHaveBeenCalledWith(expect.any(Function))
|
||||
|
||||
const updater = setSelectedCalendars.mock.calls[0][0];
|
||||
expect(updater([])).toEqual(["user1/cal1"]);
|
||||
});
|
||||
const updater = setSelectedCalendars.mock.calls[0][0]
|
||||
expect(updater([])).toEqual(['user1/cal1'])
|
||||
})
|
||||
|
||||
it("removes calendar from selection if already selected", () => {
|
||||
const setSelectedCalendars = jest.fn();
|
||||
it('removes calendar from selection if already selected', () => {
|
||||
const setSelectedCalendars = jest.fn()
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={["user1/cal1"]}
|
||||
selectedCalendars={['user1/cal1']}
|
||||
setSelectedCalendars={setSelectedCalendars}
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const checkbox = screen.getByLabelText("Calendar personal");
|
||||
fireEvent.click(checkbox);
|
||||
const checkbox = screen.getByLabelText('Calendar personal')
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
const updater = setSelectedCalendars.mock.calls[0][0];
|
||||
expect(updater(["user1/cal1"])).toEqual([]);
|
||||
});
|
||||
const updater = setSelectedCalendars.mock.calls[0][0]
|
||||
expect(updater(['user1/cal1'])).toEqual([])
|
||||
})
|
||||
|
||||
it("opens CalendarPopover modal when personal Add button is clicked", async () => {
|
||||
it('opens CalendarPopover modal when personal Add button is clicked', async () => {
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
@@ -125,26 +125,26 @@ describe("CalendarSelection", () => {
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const addButtons = screen.getAllByRole("button");
|
||||
fireEvent.click(addButtons[1]);
|
||||
const addButtons = screen.getAllByRole('button')
|
||||
fireEvent.click(addButtons[1])
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText("calendarPopover.tabs.addNew")
|
||||
screen.getByText('calendarPopover.tabs.addNew')
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("Navigates to deletion dialog and deletes personal cal", async () => {
|
||||
it('Navigates to deletion dialog and deletes personal cal', async () => {
|
||||
const spy = jest
|
||||
.spyOn(calendarThunks, "removeCalendarAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
.spyOn(calendarThunks, 'removeCalendarAsync')
|
||||
.mockImplementation(payload => {
|
||||
return () => Promise.resolve(payload) as any
|
||||
})
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
@@ -152,31 +152,31 @@ describe("CalendarSelection", () => {
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const addButtons = screen.getAllByTestId("MoreHorizIcon");
|
||||
fireEvent.click(addButtons[0]);
|
||||
const addButtons = screen.getAllByTestId('MoreHorizIcon')
|
||||
fireEvent.click(addButtons[0])
|
||||
|
||||
userEvent.click(screen.getByText(/delete/i));
|
||||
userEvent.click(screen.getByText(/delete/i))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText("calendar.delete.title(name=Calendar personal)")
|
||||
screen.getByText('calendar.delete.title(name=Calendar personal)')
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /delete/i }));
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: /delete/i }))
|
||||
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled());
|
||||
});
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled())
|
||||
})
|
||||
|
||||
it("Navigates to deletion dialog and deletes other cal", async () => {
|
||||
it('Navigates to deletion dialog and deletes other cal', async () => {
|
||||
const spy = jest
|
||||
.spyOn(calendarThunks, "removeCalendarAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
.spyOn(calendarThunks, 'removeCalendarAsync')
|
||||
.mockImplementation(payload => {
|
||||
return () => Promise.resolve(payload) as any
|
||||
})
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
@@ -184,26 +184,26 @@ describe("CalendarSelection", () => {
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const addButtons = screen.getAllByTestId("MoreHorizIcon");
|
||||
fireEvent.click(addButtons[1]);
|
||||
const addButtons = screen.getAllByTestId('MoreHorizIcon')
|
||||
fireEvent.click(addButtons[1])
|
||||
|
||||
userEvent.click(screen.getByText(/remove/i));
|
||||
userEvent.click(screen.getByText(/remove/i))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText("calendar.delete.title(name=Calendar delegated)")
|
||||
screen.getByText('calendar.delete.title(name=Calendar delegated)')
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /remove/i }));
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: /remove/i }))
|
||||
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled());
|
||||
});
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled())
|
||||
})
|
||||
|
||||
it("opens CalendarSearch modal when Other Add button is clicked", () => {
|
||||
it('opens CalendarSearch modal when Other Add button is clicked', () => {
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
@@ -211,19 +211,19 @@ describe("CalendarSelection", () => {
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const addButtons = screen.getAllByTestId("AddIcon");
|
||||
fireEvent.click(addButtons[1]); // seccond Add button (other)
|
||||
const addButtons = screen.getAllByTestId('AddIcon')
|
||||
fireEvent.click(addButtons[1]) // seccond Add button (other)
|
||||
|
||||
expect(
|
||||
screen.getByText("calendar.browseOtherCalendars")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getByText('calendar.browseOtherCalendars')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("when only calendar.personal are in the state, only calendar.personal and the title for other to be added are shown", () => {
|
||||
it('when only calendar.personal are in the state, only calendar.personal and the title for other to be added are shown', () => {
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
@@ -233,19 +233,19 @@ describe("CalendarSelection", () => {
|
||||
user: baseUser,
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": calendarsMock["user1/cal1"],
|
||||
'user1/cal1': calendarsMock['user1/cal1']
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("calendar.personal")).toBeInTheDocument();
|
||||
expect(screen.queryByText("calendar.delegated")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("calendar.other")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('calendar.personal')).toBeInTheDocument()
|
||||
expect(screen.queryByText('calendar.delegated')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('calendar.other')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders nothing when no calendars are present", () => {
|
||||
it('renders nothing when no calendars are present', () => {
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
@@ -253,14 +253,14 @@ describe("CalendarSelection", () => {
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: {}, pending: false },
|
||||
calendars: { list: {}, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.queryByLabelText(/Calendar/)).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByLabelText(/Calendar/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("expands and collapses accordions when clicked", () => {
|
||||
it('expands and collapses accordions when clicked', () => {
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
@@ -268,31 +268,31 @@ describe("CalendarSelection", () => {
|
||||
/>,
|
||||
{
|
||||
user: baseUser,
|
||||
calendars: { list: calendarsMock, pending: false },
|
||||
calendars: { list: calendarsMock, pending: false }
|
||||
}
|
||||
);
|
||||
)
|
||||
const delegatedAccordionSummary = screen
|
||||
.getByText("calendar.delegated")
|
||||
.closest(".MuiAccordionSummary-root");
|
||||
.getByText('calendar.delegated')
|
||||
.closest('.MuiAccordionSummary-root')
|
||||
|
||||
fireEvent.click(delegatedAccordionSummary!);
|
||||
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "false");
|
||||
fireEvent.click(delegatedAccordionSummary!)
|
||||
expect(delegatedAccordionSummary).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
fireEvent.click(delegatedAccordionSummary!);
|
||||
expect(delegatedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
fireEvent.click(delegatedAccordionSummary!)
|
||||
expect(delegatedAccordionSummary).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
it("renders owner name caption for non-personal, non-default calendars", () => {
|
||||
it('renders owner name caption for non-personal, non-default calendars', () => {
|
||||
renderWithProviders(
|
||||
<CalendarSelection
|
||||
selectedCalendars={[]}
|
||||
setSelectedCalendars={jest.fn()}
|
||||
/>,
|
||||
{ user: baseUser, calendars: { list: calendarsMock, pending: false } }
|
||||
);
|
||||
expect(screen.getByText("Bob Builder")).toBeInTheDocument();
|
||||
expect(screen.getByText("Charlie Chaplin")).toBeInTheDocument();
|
||||
)
|
||||
expect(screen.getByText('Bob Builder')).toBeInTheDocument()
|
||||
expect(screen.getByText('Charlie Chaplin')).toBeInTheDocument()
|
||||
// personal calendar should NOT show a caption
|
||||
expect(screen.queryByText("alice")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByText('alice')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,343 +1,331 @@
|
||||
import {
|
||||
DateTimeFields,
|
||||
DateTimeFieldsProps,
|
||||
} from "@/components/Event/components/DateTimeFields";
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
DateTimeFieldsProps
|
||||
} from '@/components/Event/components/DateTimeFields'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
jest.mock("twake-i18n", () => ({
|
||||
jest.mock('twake-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
lang: "en",
|
||||
}),
|
||||
}));
|
||||
lang: 'en'
|
||||
})
|
||||
}))
|
||||
|
||||
describe("DateTimeFields", () => {
|
||||
describe('DateTimeFields', () => {
|
||||
const mockHandlers = {
|
||||
onStartDateChange: jest.fn(),
|
||||
onStartTimeChange: jest.fn(),
|
||||
onEndDateChange: jest.fn(),
|
||||
onEndTimeChange: jest.fn(),
|
||||
onToggleEndDate: jest.fn(),
|
||||
};
|
||||
onToggleEndDate: jest.fn()
|
||||
}
|
||||
|
||||
const defaultProps: DateTimeFieldsProps = {
|
||||
startDate: "2025-07-18",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-07-18",
|
||||
endTime: "10:00",
|
||||
startDate: '2025-07-18',
|
||||
startTime: '09:00',
|
||||
endDate: '2025-07-18',
|
||||
endTime: '10:00',
|
||||
allday: false,
|
||||
showMore: true,
|
||||
hasEndDateChanged: false,
|
||||
showEndDate: false,
|
||||
validation: {
|
||||
errors: {
|
||||
dateTime: "",
|
||||
},
|
||||
dateTime: ''
|
||||
}
|
||||
},
|
||||
...mockHandlers,
|
||||
};
|
||||
...mockHandlers
|
||||
}
|
||||
|
||||
const renderField = async (props: Partial<DateTimeFieldsProps> = {}) => {
|
||||
await act(async () =>
|
||||
render(<DateTimeFields {...defaultProps} {...props} />)
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("moves END forward when START moves after END (normal mode)", async () => {
|
||||
it('moves END forward when START moves after END (normal mode)', async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "10:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "11:00",
|
||||
showMore: true,
|
||||
});
|
||||
startDate: '2025-01-01',
|
||||
startTime: '10:00',
|
||||
endDate: '2025-01-01',
|
||||
endTime: '11:00',
|
||||
showMore: true
|
||||
})
|
||||
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const startTimeInput = screen.getByTestId('start-time-input')
|
||||
|
||||
fireEvent.change(startTimeInput, { target: { value: "12:00" } });
|
||||
fireEvent.blur(startTimeInput);
|
||||
fireEvent.change(startTimeInput, { target: { value: '12:00' } })
|
||||
fireEvent.blur(startTimeInput)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith("12:00")
|
||||
);
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith('12:00')
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith("13:00")
|
||||
);
|
||||
});
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith('13:00')
|
||||
)
|
||||
})
|
||||
|
||||
it("moves END forward by full duration when START date jumps after END date", async () => {
|
||||
it('moves END forward by full duration when START date jumps after END date', async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "10:00",
|
||||
showMore: true,
|
||||
});
|
||||
startDate: '2025-01-01',
|
||||
startTime: '09:00',
|
||||
endDate: '2025-01-01',
|
||||
endTime: '10:00',
|
||||
showMore: true
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByTestId("start-date-input"));
|
||||
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
||||
await userEvent.click(dayButton);
|
||||
await userEvent.click(screen.getByTestId('start-date-input'))
|
||||
const dayButton = screen.getByRole('gridcell', { name: '3' })
|
||||
await userEvent.click(dayButton)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith('2025-01-03')
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
});
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith('2025-01-03')
|
||||
)
|
||||
})
|
||||
|
||||
it("does NOT move START backward when END moves before START (normal mode)", async () => {
|
||||
it('does NOT move START backward when END moves before START (normal mode)', async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "10:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "11:00",
|
||||
showMore: true,
|
||||
});
|
||||
startDate: '2025-01-01',
|
||||
startTime: '10:00',
|
||||
endDate: '2025-01-01',
|
||||
endTime: '11:00',
|
||||
showMore: true
|
||||
})
|
||||
|
||||
const endTimeInput = screen.getByTestId("end-time-input");
|
||||
const endTimeInput = screen.getByTestId('end-time-input')
|
||||
|
||||
fireEvent.change(endTimeInput, { target: { value: "08:00" } });
|
||||
fireEvent.blur(endTimeInput);
|
||||
fireEvent.change(endTimeInput, { target: { value: '08:00' } })
|
||||
fireEvent.blur(endTimeInput)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith("08:00")
|
||||
);
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith('08:00')
|
||||
)
|
||||
|
||||
// Start time should NOT be automatically adjusted when end time changes
|
||||
expect(mockHandlers.onStartTimeChange).not.toHaveBeenCalled();
|
||||
expect(mockHandlers.onStartDateChange).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockHandlers.onStartTimeChange).not.toHaveBeenCalled()
|
||||
expect(mockHandlers.onStartDateChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("moves START backward properly when END date jumps before START date", async () => {
|
||||
it('moves START backward properly when END date jumps before START date', async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-05",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-01-05",
|
||||
endTime: "10:00",
|
||||
showMore: true,
|
||||
});
|
||||
startDate: '2025-01-05',
|
||||
startTime: '09:00',
|
||||
endDate: '2025-01-05',
|
||||
endTime: '10:00',
|
||||
showMore: true
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByTestId("end-date-input"));
|
||||
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
||||
await userEvent.click(dayButton);
|
||||
await userEvent.click(screen.getByTestId('end-date-input'))
|
||||
const dayButton = screen.getByRole('gridcell', { name: '3' })
|
||||
await userEvent.click(dayButton)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith('2025-01-03')
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-01-03")
|
||||
);
|
||||
});
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith('2025-01-03')
|
||||
)
|
||||
})
|
||||
|
||||
it("pushes END forward in whole days for allday events", async () => {
|
||||
it('pushes END forward in whole days for allday events', async () => {
|
||||
await renderField({
|
||||
allday: true,
|
||||
startDate: "2025-02-01",
|
||||
endDate: "2025-02-03",
|
||||
showEndDate: true,
|
||||
});
|
||||
startDate: '2025-02-01',
|
||||
endDate: '2025-02-03',
|
||||
showEndDate: true
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByTestId("start-date-input"));
|
||||
const dayButton = screen.getByRole("gridcell", { name: "10" });
|
||||
await userEvent.click(dayButton);
|
||||
await userEvent.click(screen.getByTestId('start-date-input'))
|
||||
const dayButton = screen.getByRole('gridcell', { name: '10' })
|
||||
await userEvent.click(dayButton)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-02-10")
|
||||
);
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith('2025-02-10')
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-02-12")
|
||||
);
|
||||
});
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith('2025-02-12')
|
||||
)
|
||||
})
|
||||
|
||||
it("moves START backward in whole days for allday when END moves earlier", async () => {
|
||||
it('moves START backward in whole days for allday when END moves earlier', async () => {
|
||||
await renderField({
|
||||
allday: true,
|
||||
startDate: "2025-05-10",
|
||||
endDate: "2025-05-15",
|
||||
showEndDate: true,
|
||||
});
|
||||
startDate: '2025-05-10',
|
||||
endDate: '2025-05-15',
|
||||
showEndDate: true
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByTestId("end-date-input"));
|
||||
const dayButton = screen.getByRole("gridcell", { name: "1" });
|
||||
await userEvent.click(dayButton);
|
||||
await userEvent.click(screen.getByTestId('end-date-input'))
|
||||
const dayButton = screen.getByRole('gridcell', { name: '1' })
|
||||
await userEvent.click(dayButton)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith("2025-05-01")
|
||||
);
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith('2025-05-01')
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith("2025-04-26")
|
||||
);
|
||||
});
|
||||
expect(mockHandlers.onStartDateChange).toHaveBeenCalledWith('2025-04-26')
|
||||
)
|
||||
})
|
||||
|
||||
it("does not call handlers if invalid (null) date value", async () => {
|
||||
it('does not call handlers if invalid (null) date value', async () => {
|
||||
await renderField({
|
||||
showMore: true,
|
||||
});
|
||||
showMore: true
|
||||
})
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const startDateInput = screen.getByTestId('start-date-input')
|
||||
|
||||
fireEvent.change(startDateInput, { target: { value: "" } });
|
||||
fireEvent.change(startDateInput, { target: { value: '' } })
|
||||
|
||||
expect(mockHandlers.onStartDateChange).not.toHaveBeenCalled();
|
||||
expect(mockHandlers.onEndDateChange).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockHandlers.onStartDateChange).not.toHaveBeenCalled()
|
||||
expect(mockHandlers.onEndDateChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("shift to preserve original duration (normal case)", async () => {
|
||||
it('shift to preserve original duration (normal case)', async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "09:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "10:00",
|
||||
showMore: true,
|
||||
});
|
||||
startDate: '2025-01-01',
|
||||
startTime: '09:00',
|
||||
endDate: '2025-01-01',
|
||||
endTime: '10:00',
|
||||
showMore: true
|
||||
})
|
||||
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const startTimeInput = screen.getByTestId('start-time-input')
|
||||
|
||||
fireEvent.change(startTimeInput, { target: { value: "09:30" } });
|
||||
fireEvent.blur(startTimeInput);
|
||||
fireEvent.change(startTimeInput, { target: { value: '09:30' } })
|
||||
fireEvent.blur(startTimeInput)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith("09:30")
|
||||
);
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith('09:30')
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith("10:30")
|
||||
);
|
||||
});
|
||||
expect(mockHandlers.onEndTimeChange).toHaveBeenCalledWith('10:30')
|
||||
)
|
||||
})
|
||||
|
||||
it("preserves 1-hour duration across midnight when changing start time from 22:30 to 23:45", async () => {
|
||||
it('preserves 1-hour duration across midnight when changing start time from 22:30 to 23:45', async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-15",
|
||||
startTime: "22:30",
|
||||
endDate: "2025-01-15",
|
||||
endTime: "23:30",
|
||||
showMore: true,
|
||||
});
|
||||
startDate: '2025-01-15',
|
||||
startTime: '22:30',
|
||||
endDate: '2025-01-15',
|
||||
endTime: '23:30',
|
||||
showMore: true
|
||||
})
|
||||
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const startTimeInput = screen.getByTestId('start-time-input')
|
||||
|
||||
// Change start time from 22:30 to 23:45
|
||||
fireEvent.change(startTimeInput, { target: { value: "23:45" } });
|
||||
fireEvent.blur(startTimeInput);
|
||||
fireEvent.change(startTimeInput, { target: { value: '23:45' } })
|
||||
fireEvent.blur(startTimeInput)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith("23:45")
|
||||
);
|
||||
expect(mockHandlers.onStartTimeChange).toHaveBeenCalledWith('23:45')
|
||||
)
|
||||
|
||||
// End date should move to the next day
|
||||
await waitFor(() =>
|
||||
expect(mockHandlers.onEndDateChange).toHaveBeenCalledWith(
|
||||
"2025-01-16",
|
||||
"00:45"
|
||||
'2025-01-16',
|
||||
'00:45'
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should have aria-label for accessibility and testing", async () => {
|
||||
it('should have aria-label for accessibility and testing', async () => {
|
||||
await renderField({
|
||||
startDate: "2025-01-01",
|
||||
startTime: "10:00",
|
||||
endDate: "2025-01-01",
|
||||
endTime: "11:00",
|
||||
showMore: true,
|
||||
});
|
||||
startDate: '2025-01-01',
|
||||
startTime: '10:00',
|
||||
endDate: '2025-01-01',
|
||||
endTime: '11:00',
|
||||
showMore: true
|
||||
})
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const endDateInput = screen.getByTestId("end-date-input");
|
||||
const endTimeInput = screen.getByTestId("end-time-input");
|
||||
const startDateInput = screen.getByTestId('start-date-input')
|
||||
const startTimeInput = screen.getByTestId('start-time-input')
|
||||
const endDateInput = screen.getByTestId('end-date-input')
|
||||
const endTimeInput = screen.getByTestId('end-time-input')
|
||||
|
||||
expect(startDateInput).toHaveAttribute(
|
||||
"aria-label",
|
||||
"dateTimeFields.startDate"
|
||||
);
|
||||
'aria-label',
|
||||
'dateTimeFields.startDate'
|
||||
)
|
||||
expect(startTimeInput).toHaveAttribute(
|
||||
"aria-label",
|
||||
"dateTimeFields.startTime"
|
||||
);
|
||||
expect(endDateInput).toHaveAttribute(
|
||||
"aria-label",
|
||||
"dateTimeFields.endDate"
|
||||
);
|
||||
expect(endTimeInput).toHaveAttribute(
|
||||
"aria-label",
|
||||
"dateTimeFields.endTime"
|
||||
);
|
||||
});
|
||||
'aria-label',
|
||||
'dateTimeFields.startTime'
|
||||
)
|
||||
expect(endDateInput).toHaveAttribute('aria-label', 'dateTimeFields.endDate')
|
||||
expect(endTimeInput).toHaveAttribute('aria-label', 'dateTimeFields.endTime')
|
||||
})
|
||||
|
||||
describe("DateTimeFields - Drag and Drop Display Logic", () => {
|
||||
describe('DateTimeFields - Drag and Drop Display Logic', () => {
|
||||
// Test 1.1: Display 2 fields when drag from allday slot (multiple days)
|
||||
it("displays only 2 date fields when allday=true and multiple days", async () => {
|
||||
it('displays only 2 date fields when allday=true and multiple days', async () => {
|
||||
await renderField({
|
||||
allday: true,
|
||||
hasEndDateChanged: false,
|
||||
startDate: "2025-07-18",
|
||||
endDate: "2025-07-20",
|
||||
startDate: '2025-07-18',
|
||||
endDate: '2025-07-20',
|
||||
showEndDate: true,
|
||||
showMore: false,
|
||||
});
|
||||
showMore: false
|
||||
})
|
||||
|
||||
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 1.2: Display 4 fields when drag from week view (multiple days)
|
||||
it("displays 4 fields when allday=false, hasEndDateChanged=true, and multiple days", async () => {
|
||||
it('displays 4 fields when allday=false, hasEndDateChanged=true, and multiple days', async () => {
|
||||
await renderField({
|
||||
allday: false,
|
||||
hasEndDateChanged: true,
|
||||
startDate: "2025-07-18",
|
||||
endDate: "2025-07-20",
|
||||
startTime: "09:00",
|
||||
endTime: "10:00",
|
||||
startDate: '2025-07-18',
|
||||
endDate: '2025-07-20',
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
showEndDate: true,
|
||||
showMore: false,
|
||||
});
|
||||
showMore: false
|
||||
})
|
||||
|
||||
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 1.3: Display single date + time fields
|
||||
it("displays single date field with time fields for single day event", async () => {
|
||||
it('displays single date field with time fields for single day event', async () => {
|
||||
await renderField({
|
||||
allday: false,
|
||||
hasEndDateChanged: false,
|
||||
startDate: "2025-07-18",
|
||||
endDate: "2025-07-18",
|
||||
startTime: "09:00",
|
||||
endTime: "10:00",
|
||||
startDate: '2025-07-18',
|
||||
endDate: '2025-07-18',
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
showEndDate: false,
|
||||
showMore: false,
|
||||
});
|
||||
showMore: false
|
||||
})
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const startDateInput = screen.getByTestId('start-date-input')
|
||||
expect(startDateInput).toHaveAttribute(
|
||||
"aria-label",
|
||||
"dateTimeFields.date"
|
||||
);
|
||||
expect(screen.getByTestId("start-time-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("end-time-input")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("end-date-input")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
'aria-label',
|
||||
'dateTimeFields.date'
|
||||
)
|
||||
expect(screen.getByTestId('start-time-input')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('end-time-input')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('end-date-input')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
import { RootState } from "@/app/store";
|
||||
import EventDuplication from "@/components/Event/EventDuplicate";
|
||||
import EventPreviewModal from "@/features/Events/EventPreview";
|
||||
import EventPopover from "@/features/Events/EventModal";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import { RootState } from '@/app/store'
|
||||
import EventDuplication from '@/components/Event/EventDuplicate'
|
||||
import EventPreviewModal from '@/features/Events/EventPreview'
|
||||
import EventPopover from '@/features/Events/EventModal'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
const day = new Date();
|
||||
const day = new Date()
|
||||
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: "mailto:test@test.com",
|
||||
},
|
||||
cn: 'test',
|
||||
cal_address: 'mailto:test@test.com'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Calendar",
|
||||
color: "#FF0000",
|
||||
owner: { emails: ["test@test.com"] },
|
||||
'667037022b752d0026472254/cal1': {
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
name: 'Calendar',
|
||||
color: '#FF0000',
|
||||
owner: { emails: ['test@test.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
URL: "calendars/667037022b752d0026472254/cal1/event1.ics",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: 'event1',
|
||||
URL: 'calendars/667037022b752d0026472254/cal1/event1.ics',
|
||||
title: 'Test Event',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
start: day.toISOString(),
|
||||
end: day.toISOString(),
|
||||
timezone: "UTC",
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
timezone: 'UTC',
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
} as unknown as RootState;
|
||||
pending: false
|
||||
}
|
||||
} as unknown as RootState
|
||||
|
||||
describe("EventDuplication", () => {
|
||||
it("calls onOpenDuplicate when button clicked", () => {
|
||||
const handleClose = jest.fn();
|
||||
const onOpenDuplicate = jest.fn();
|
||||
describe('EventDuplication', () => {
|
||||
it('calls onOpenDuplicate when button clicked', () => {
|
||||
const handleClose = jest.fn()
|
||||
const onOpenDuplicate = jest.fn()
|
||||
renderWithProviders(
|
||||
<EventDuplication
|
||||
event={
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
|
||||
preloadedState.calendars.list['667037022b752d0026472254/cal1'].events
|
||||
.event1
|
||||
}
|
||||
onClose={handleClose}
|
||||
onOpenDuplicate={onOpenDuplicate}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("menuitem", { name: "eventDuplication.duplicateEvent" })
|
||||
);
|
||||
screen.getByRole('menuitem', { name: 'eventDuplication.duplicateEvent' })
|
||||
)
|
||||
|
||||
expect(onOpenDuplicate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(onOpenDuplicate).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("EventPopover", () => {
|
||||
it("renders with event data", () => {
|
||||
describe('EventPopover', () => {
|
||||
it('renders with event data', () => {
|
||||
renderWithProviders(
|
||||
<EventPopover
|
||||
anchorEl={null}
|
||||
@@ -97,18 +97,18 @@ describe("EventPopover", () => {
|
||||
setSelectedRange={jest.fn()}
|
||||
calendarRef={{ current: null }}
|
||||
event={
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
|
||||
preloadedState.calendars.list['667037022b752d0026472254/cal1'].events
|
||||
.event1
|
||||
}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByDisplayValue(/Test Event/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByDisplayValue(/Test Event/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("saves duplicated event when Save is clicked", () => {
|
||||
const onClose = jest.fn();
|
||||
it('saves duplicated event when Save is clicked', () => {
|
||||
const onClose = jest.fn()
|
||||
|
||||
renderWithProviders(
|
||||
<EventPopover
|
||||
@@ -119,24 +119,24 @@ describe("EventPopover", () => {
|
||||
setSelectedRange={jest.fn()}
|
||||
calendarRef={{ current: null }}
|
||||
event={
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
|
||||
preloadedState.calendars.list['667037022b752d0026472254/cal1'].events
|
||||
.event1
|
||||
}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/Title/i), {
|
||||
target: { value: "Duplicated Event" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
target: { value: 'Duplicated Event' }
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
|
||||
waitFor(() => expect(onClose).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
waitFor(() => expect(onClose).toHaveBeenCalled())
|
||||
})
|
||||
})
|
||||
|
||||
describe("EventDisplayModal", () => {
|
||||
it("shows duplication button and opens duplication form", () => {
|
||||
describe('EventDisplayModal', () => {
|
||||
it('shows duplication button and opens duplication form', () => {
|
||||
renderWithProviders(
|
||||
<EventPreviewModal
|
||||
eventId="event1"
|
||||
@@ -145,20 +145,20 @@ describe("EventDisplayModal", () => {
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId("MoreVertIcon"));
|
||||
fireEvent.click(screen.getByTestId('MoreVertIcon'))
|
||||
fireEvent.click(
|
||||
screen.getByRole("menuitem", { name: "eventDuplication.duplicateEvent" })
|
||||
);
|
||||
screen.getByRole('menuitem', { name: 'eventDuplication.duplicateEvent' })
|
||||
)
|
||||
expect(
|
||||
screen.getAllByText("eventDuplication.duplicateEvent")[1]
|
||||
).toBeInTheDocument();
|
||||
screen.getAllByText('eventDuplication.duplicateEvent')[1]
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByDisplayValue(
|
||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
|
||||
preloadedState.calendars.list['667037022b752d0026472254/cal1'].events
|
||||
.event1.title as string
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,176 +3,174 @@ import {
|
||||
convertFormDateTimeToISO,
|
||||
DATETIME_WITH_SECONDS_LENGTH,
|
||||
DATETIME_FORMAT_WITH_SECONDS,
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS,
|
||||
} from "@/components/Event/utils/dateTimeHelpers";
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS
|
||||
} from '@/components/Event/utils/dateTimeHelpers'
|
||||
|
||||
describe("dateTimeHelpers", () => {
|
||||
describe("Constants", () => {
|
||||
it("should have correct constant values", () => {
|
||||
expect(DATETIME_WITH_SECONDS_LENGTH).toBe(19);
|
||||
expect(DATETIME_FORMAT_WITH_SECONDS).toBe("YYYY-MM-DDTHH:mm:ss");
|
||||
expect(DATETIME_FORMAT_WITHOUT_SECONDS).toBe("YYYY-MM-DDTHH:mm");
|
||||
});
|
||||
});
|
||||
describe('dateTimeHelpers', () => {
|
||||
describe('Constants', () => {
|
||||
it('should have correct constant values', () => {
|
||||
expect(DATETIME_WITH_SECONDS_LENGTH).toBe(19)
|
||||
expect(DATETIME_FORMAT_WITH_SECONDS).toBe('YYYY-MM-DDTHH:mm:ss')
|
||||
expect(DATETIME_FORMAT_WITHOUT_SECONDS).toBe('YYYY-MM-DDTHH:mm')
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectDateTimeFormat", () => {
|
||||
it("should return format with seconds for length >= 19", () => {
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30:45")).toBe(
|
||||
describe('detectDateTimeFormat', () => {
|
||||
it('should return format with seconds for length >= 19', () => {
|
||||
expect(detectDateTimeFormat('2024-01-15T10:30:45')).toBe(
|
||||
DATETIME_FORMAT_WITH_SECONDS
|
||||
);
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30:45")).toBe(
|
||||
"YYYY-MM-DDTHH:mm:ss"
|
||||
);
|
||||
});
|
||||
)
|
||||
expect(detectDateTimeFormat('2024-01-15T10:30:45')).toBe(
|
||||
'YYYY-MM-DDTHH:mm:ss'
|
||||
)
|
||||
})
|
||||
|
||||
it("should return format without seconds for length < 19", () => {
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30")).toBe(
|
||||
it('should return format without seconds for length < 19', () => {
|
||||
expect(detectDateTimeFormat('2024-01-15T10:30')).toBe(
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS
|
||||
);
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30")).toBe("YYYY-MM-DDTHH:mm");
|
||||
});
|
||||
)
|
||||
expect(detectDateTimeFormat('2024-01-15T10:30')).toBe('YYYY-MM-DDTHH:mm')
|
||||
})
|
||||
|
||||
it("should return format with seconds for length exactly 19", () => {
|
||||
const datetime = "2024-01-15T10:30:45";
|
||||
expect(datetime.length).toBe(19);
|
||||
expect(detectDateTimeFormat(datetime)).toBe(DATETIME_FORMAT_WITH_SECONDS);
|
||||
});
|
||||
it('should return format with seconds for length exactly 19', () => {
|
||||
const datetime = '2024-01-15T10:30:45'
|
||||
expect(datetime.length).toBe(19)
|
||||
expect(detectDateTimeFormat(datetime)).toBe(DATETIME_FORMAT_WITH_SECONDS)
|
||||
})
|
||||
|
||||
it("should return format without seconds for length 16", () => {
|
||||
const datetime = "2024-01-15T10:30";
|
||||
expect(datetime.length).toBe(16);
|
||||
it('should return format without seconds for length 16', () => {
|
||||
const datetime = '2024-01-15T10:30'
|
||||
expect(datetime.length).toBe(16)
|
||||
expect(detectDateTimeFormat(datetime)).toBe(
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should return format with seconds for length > 19", () => {
|
||||
const datetime = "2024-01-15T10:30:45.123";
|
||||
expect(datetime.length).toBeGreaterThan(19);
|
||||
expect(detectDateTimeFormat(datetime)).toBe(DATETIME_FORMAT_WITH_SECONDS);
|
||||
});
|
||||
it('should return format with seconds for length > 19', () => {
|
||||
const datetime = '2024-01-15T10:30:45.123'
|
||||
expect(datetime.length).toBeGreaterThan(19)
|
||||
expect(detectDateTimeFormat(datetime)).toBe(DATETIME_FORMAT_WITH_SECONDS)
|
||||
})
|
||||
|
||||
it("should handle empty string", () => {
|
||||
expect(detectDateTimeFormat("")).toBe(DATETIME_FORMAT_WITHOUT_SECONDS);
|
||||
});
|
||||
it('should handle empty string', () => {
|
||||
expect(detectDateTimeFormat('')).toBe(DATETIME_FORMAT_WITHOUT_SECONDS)
|
||||
})
|
||||
|
||||
it("should handle very short strings", () => {
|
||||
expect(detectDateTimeFormat("2024")).toBe(
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should handle very short strings', () => {
|
||||
expect(detectDateTimeFormat('2024')).toBe(DATETIME_FORMAT_WITHOUT_SECONDS)
|
||||
})
|
||||
})
|
||||
|
||||
describe("convertFormDateTimeToISO", () => {
|
||||
const originalConsoleWarn = console.warn;
|
||||
let consoleWarnSpy: jest.SpyInstance;
|
||||
describe('convertFormDateTimeToISO', () => {
|
||||
const originalConsoleWarn = console.warn
|
||||
let consoleWarnSpy: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
});
|
||||
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should convert valid datetime without seconds to ISO string", () => {
|
||||
it('should convert valid datetime without seconds to ISO string', () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
'2024-01-15T10:30',
|
||||
'America/New_York'
|
||||
)
|
||||
expect(result).toBeTruthy()
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should convert valid datetime with seconds to ISO string", () => {
|
||||
it('should convert valid datetime with seconds to ISO string', () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30:45",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
'2024-01-15T10:30:45',
|
||||
'America/New_York'
|
||||
)
|
||||
expect(result).toBeTruthy()
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should use Etc/UTC as default timezone when timezone is empty", () => {
|
||||
const result = convertFormDateTimeToISO("2024-01-15T10:30", "");
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
it('should use Etc/UTC as default timezone when timezone is empty', () => {
|
||||
const result = convertFormDateTimeToISO('2024-01-15T10:30', '')
|
||||
expect(result).toBeTruthy()
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
})
|
||||
|
||||
it("should return empty string for empty datetime input", () => {
|
||||
const result = convertFormDateTimeToISO("", "America/New_York");
|
||||
expect(result).toBe("");
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should return empty string for empty datetime input', () => {
|
||||
const result = convertFormDateTimeToISO('', 'America/New_York')
|
||||
expect(result).toBe('')
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return empty string and log warning for invalid datetime", () => {
|
||||
it('should return empty string and log warning for invalid datetime', () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"invalid-date",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBe("");
|
||||
'invalid-date',
|
||||
'America/New_York'
|
||||
)
|
||||
expect(result).toBe('')
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[convertFormDateTimeToISO] Invalid datetime:")
|
||||
);
|
||||
expect.stringContaining('[convertFormDateTimeToISO] Invalid datetime:')
|
||||
)
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"invalid-date"')
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should return empty string and log warning for invalid format", () => {
|
||||
it('should return empty string and log warning for invalid format', () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-13-45T25:99:99",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBe("");
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
'2024-13-45T25:99:99',
|
||||
'America/New_York'
|
||||
)
|
||||
expect(result).toBe('')
|
||||
expect(consoleWarnSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle different timezones correctly", () => {
|
||||
it('should handle different timezones correctly', () => {
|
||||
const result1 = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
"America/New_York"
|
||||
);
|
||||
'2024-01-15T10:30',
|
||||
'America/New_York'
|
||||
)
|
||||
const result2 = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
"Europe/London"
|
||||
);
|
||||
expect(result1).toBeTruthy();
|
||||
expect(result2).toBeTruthy();
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
'2024-01-15T10:30',
|
||||
'Europe/London'
|
||||
)
|
||||
expect(result1).toBeTruthy()
|
||||
expect(result2).toBeTruthy()
|
||||
expect(result1).not.toBe(result2)
|
||||
})
|
||||
|
||||
it("should handle edge case with null/undefined timezone", () => {
|
||||
it('should handle edge case with null/undefined timezone', () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
'2024-01-15T10:30',
|
||||
// @ts-ignore - testing edge case
|
||||
null
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should convert correctly for UTC timezone", () => {
|
||||
const result = convertFormDateTimeToISO("2024-01-15T10:30", "Etc/UTC");
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toContain("T10:30:00");
|
||||
});
|
||||
it('should convert correctly for UTC timezone', () => {
|
||||
const result = convertFormDateTimeToISO('2024-01-15T10:30', 'Etc/UTC')
|
||||
expect(result).toBeTruthy()
|
||||
expect(result).toContain('T10:30:00')
|
||||
})
|
||||
|
||||
it("should handle datetime at boundary (exactly 19 characters)", () => {
|
||||
const datetime = "2024-01-15T10:30:45";
|
||||
expect(datetime.length).toBe(19);
|
||||
const result = convertFormDateTimeToISO(datetime, "Etc/UTC");
|
||||
expect(result).toBeTruthy();
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should handle datetime at boundary (exactly 19 characters)', () => {
|
||||
const datetime = '2024-01-15T10:30:45'
|
||||
expect(datetime.length).toBe(19)
|
||||
const result = convertFormDateTimeToISO(datetime, 'Etc/UTC')
|
||||
expect(result).toBeTruthy()
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle datetime at boundary (exactly 16 characters)", () => {
|
||||
const datetime = "2024-01-15T10:30";
|
||||
expect(datetime.length).toBe(16);
|
||||
const result = convertFormDateTimeToISO(datetime, "Etc/UTC");
|
||||
expect(result).toBeTruthy();
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should handle datetime at boundary (exactly 16 characters)', () => {
|
||||
const datetime = '2024-01-15T10:30'
|
||||
expect(datetime.length).toBe(16)
|
||||
const result = convertFormDateTimeToISO(datetime, 'Etc/UTC')
|
||||
expect(result).toBeTruthy()
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,102 +1,100 @@
|
||||
import CalendarLayout from "@/components/Calendar/CalendarLayout";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import CalendarLayout from '@/components/Calendar/CalendarLayout'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
describe("Event Error Handling", () => {
|
||||
describe('Event Error Handling', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
localStorage.clear()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const today = new Date();
|
||||
const start = new Date(today);
|
||||
start.setHours(10, 0, 0, 0);
|
||||
const end = new Date(today);
|
||||
end.setHours(11, 0, 0, 0);
|
||||
const today = new Date()
|
||||
const start = new Date(today)
|
||||
start.setHours(10, 0, 0, 0)
|
||||
const end = new Date(today)
|
||||
end.setHours(11, 0, 0, 0)
|
||||
|
||||
const erroredState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
tokens: { accessToken: "token" },
|
||||
tokens: { accessToken: 'token' }
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "Calendar personal",
|
||||
id: "user1/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'user1/cal1': {
|
||||
name: 'Calendar personal',
|
||||
id: 'user1/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user1/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: 'user1/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: start.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
it("BUGFIX: does not re-report errors after clearing error snackbar", async () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
it('BUGFIX: does not re-report errors after clearing error snackbar', async () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation()
|
||||
|
||||
await act(async () =>
|
||||
renderWithProviders(<CalendarLayout />, erroredState)
|
||||
);
|
||||
await act(async () => renderWithProviders(<CalendarLayout />, erroredState))
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText("Test Event"));
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Event'))
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
const closeButton = screen.queryByRole("button", { name: "common.ok" });
|
||||
)
|
||||
const closeButton = screen.queryByRole('button', { name: 'common.ok' })
|
||||
|
||||
if (closeButton) {
|
||||
const initialWarnCount = consoleWarnSpy.mock.calls.length;
|
||||
const initialWarnCount = consoleWarnSpy.mock.calls.length
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(closeButton);
|
||||
});
|
||||
fireEvent.click(closeButton)
|
||||
})
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
)
|
||||
|
||||
const afterCloseWarnCount = consoleWarnSpy.mock.calls.length;
|
||||
const afterCloseWarnCount = consoleWarnSpy.mock.calls.length
|
||||
|
||||
expect(afterCloseWarnCount).toBe(initialWarnCount);
|
||||
expect(afterCloseWarnCount).toBe(initialWarnCount)
|
||||
}
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
}, 15000);
|
||||
});
|
||||
consoleWarnSpy.mockRestore()
|
||||
}, 15000)
|
||||
})
|
||||
|
||||
@@ -1,350 +1,340 @@
|
||||
import * as appHooks from "@/app/hooks";
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import CalendarApp from "@/components/Calendar/Calendar";
|
||||
import * as appHooks from '@/app/hooks'
|
||||
import { AppDispatch } from '@/app/store'
|
||||
import CalendarApp from '@/components/Calendar/Calendar'
|
||||
import {
|
||||
createEventHandlers,
|
||||
EventHandlersProps,
|
||||
} from "@/components/Calendar/handlers/eventHandlers";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import { CalendarApi } from "@fullcalendar/core";
|
||||
import { jest } from "@jest/globals";
|
||||
import "@testing-library/jest-dom";
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from "@testing-library/react";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
EventHandlersProps
|
||||
} from '@/components/Calendar/handlers/eventHandlers'
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import EventUpdateModal from '@/features/Events/EventUpdateModal'
|
||||
import { CalendarApi } from '@fullcalendar/core'
|
||||
import { jest } from '@jest/globals'
|
||||
import '@testing-library/jest-dom'
|
||||
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
describe("CalendarApp integration", () => {
|
||||
const today = new Date();
|
||||
const start = new Date(today);
|
||||
start.setHours(10, 0, 0, 0);
|
||||
const end = new Date(today);
|
||||
end.setHours(11, 0, 0, 0);
|
||||
describe('CalendarApp integration', () => {
|
||||
const today = new Date()
|
||||
const start = new Date(today)
|
||||
start.setHours(10, 0, 0, 0)
|
||||
const end = new Date(today)
|
||||
end.setHours(11, 0, 0, 0)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const renderCalendar = () => {
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
tokens: { accessToken: "token" }, // required to avoid redirect
|
||||
tokens: { accessToken: 'token' } // required to avoid redirect
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
color: { light: "#FFFFFF", dark: "#000000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'667037022b752d0026472254/cal1': {
|
||||
name: 'Calendar 1',
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
color: { light: '#FFFFFF', dark: '#000000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
const mockCalendarRef = { current: null };
|
||||
const mockCalendarRef = { current: null }
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
it("renders the event on the calendar and calendarRef works", async () => {
|
||||
it('renders the event on the calendar and calendarRef works', async () => {
|
||||
const dispatch = jest.fn().mockReturnValue(
|
||||
Object.assign(Promise.resolve({}), {
|
||||
unwrap: () => Promise.resolve({}),
|
||||
unwrap: () => Promise.resolve({})
|
||||
})
|
||||
) as unknown as AppDispatch;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
) as unknown as AppDispatch
|
||||
jest.spyOn(appHooks, 'useAppDispatch').mockReturnValue(dispatch)
|
||||
|
||||
renderCalendar();
|
||||
renderCalendar()
|
||||
const calendarRef: React.RefObject<CalendarApi | null> = (window as any)
|
||||
.__calendarRef;
|
||||
.__calendarRef
|
||||
|
||||
const calendarApi = calendarRef.current;
|
||||
const calendarApi = calendarRef.current
|
||||
|
||||
// Wait for the FullCalendar DOM to populate
|
||||
const eventEl = await screen.findByText(
|
||||
"Test Event",
|
||||
{},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
expect(eventEl).toBeInTheDocument();
|
||||
const eventEl = await screen.findByText('Test Event', {}, { timeout: 3000 })
|
||||
expect(eventEl).toBeInTheDocument()
|
||||
act(() => {
|
||||
if (calendarApi) {
|
||||
const fcEvent = calendarApi.getEventById("event1");
|
||||
expect(fcEvent?.title).toBe("Test Event");
|
||||
const oldEnd = new Date(today.getTime() + 3600000); // +1 hour
|
||||
const newEnd = new Date(oldEnd.getTime() + 1800000); // +30 min
|
||||
const fcEvent = calendarApi.getEventById('event1')
|
||||
expect(fcEvent?.title).toBe('Test Event')
|
||||
const oldEnd = new Date(today.getTime() + 3600000) // +1 hour
|
||||
const newEnd = new Date(oldEnd.getTime() + 1800000) // +30 min
|
||||
|
||||
fcEvent?.setEnd(newEnd);
|
||||
fcEvent?.setEnd(newEnd)
|
||||
|
||||
waitFor(() => expect(dispatch).toHaveBeenCalled());
|
||||
waitFor(() => expect(dispatch).toHaveBeenCalled())
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
const createPreloadedState = (eventProps = {}) => ({
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
tokens: {
|
||||
accessToken: "token",
|
||||
},
|
||||
accessToken: 'token'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
color: { light: "#FFFFFF", dark: "#000000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'667037022b752d0026472254/cal1': {
|
||||
name: 'Calendar 1',
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
color: { light: '#FFFFFF', dark: '#000000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: "event1",
|
||||
id: 'event1',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
uid: 'event1',
|
||||
start: new Date().toISOString(),
|
||||
end: new Date(Date.now() + 3600000).toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
],
|
||||
...eventProps,
|
||||
},
|
||||
},
|
||||
},
|
||||
...eventProps
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
});
|
||||
pending: false
|
||||
}
|
||||
})
|
||||
|
||||
it("renders lock icon for private events on the calendar", async () => {
|
||||
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
|
||||
it('renders lock icon for private events on the calendar', async () => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
||||
configurable: true,
|
||||
value: 120,
|
||||
});
|
||||
value: 120
|
||||
})
|
||||
const preloadedState = createPreloadedState({
|
||||
class: "PRIVATE",
|
||||
title: "Private Event",
|
||||
});
|
||||
const mockCalendarRef = { current: null };
|
||||
class: 'PRIVATE',
|
||||
title: 'Private Event'
|
||||
})
|
||||
const mockCalendarRef = { current: null }
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
const card = screen.getByTestId("event-card-event1");
|
||||
const lockIcon = within(card).getByTestId("LockOutlineIcon");
|
||||
expect(lockIcon).toBeInTheDocument();
|
||||
});
|
||||
)
|
||||
const card = screen.getByTestId('event-card-event1')
|
||||
const lockIcon = within(card).getByTestId('LockOutlineIcon')
|
||||
expect(lockIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders lock icon for confidential events on the calendar", async () => {
|
||||
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
|
||||
it('renders lock icon for confidential events on the calendar', async () => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
||||
configurable: true,
|
||||
value: 120,
|
||||
});
|
||||
value: 120
|
||||
})
|
||||
const preloadedState = createPreloadedState({
|
||||
class: "CONFIDENTIAL",
|
||||
title: "Confidential Event",
|
||||
});
|
||||
const mockCalendarRef = { current: null };
|
||||
class: 'CONFIDENTIAL',
|
||||
title: 'Confidential Event'
|
||||
})
|
||||
const mockCalendarRef = { current: null }
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
const card = screen.getByTestId("event-card-event1");
|
||||
const lockIcon = within(card).getByTestId("LockOutlineIcon");
|
||||
expect(lockIcon).toBeInTheDocument();
|
||||
});
|
||||
const card = screen.getByTestId('event-card-event1')
|
||||
const lockIcon = within(card).getByTestId('LockOutlineIcon')
|
||||
expect(lockIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does NOT render a lock icon for public events on the calendar", async () => {
|
||||
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
|
||||
it('does NOT render a lock icon for public events on the calendar', async () => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
||||
configurable: true,
|
||||
value: 120,
|
||||
});
|
||||
value: 120
|
||||
})
|
||||
const preloadedState = createPreloadedState({
|
||||
class: "PUBLIC",
|
||||
title: "Public Event",
|
||||
});
|
||||
const mockCalendarRef = { current: null };
|
||||
class: 'PUBLIC',
|
||||
title: 'Public Event'
|
||||
})
|
||||
const mockCalendarRef = { current: null }
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
const card = screen.getByTestId("event-card-event1");
|
||||
const lockIcon = within(card).queryByTestId("LockOutlineIcon");
|
||||
expect(lockIcon).not.toBeInTheDocument();
|
||||
});
|
||||
const card = screen.getByTestId('event-card-event1')
|
||||
const lockIcon = within(card).queryByTestId('LockOutlineIcon')
|
||||
expect(lockIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does render a title for events without any attendees or user as organizer", async () => {
|
||||
const mockCalendarRef = { current: null };
|
||||
it('does render a title for events without any attendees or user as organizer', async () => {
|
||||
const mockCalendarRef = { current: null }
|
||||
renderWithProviders(<CalendarApp calendarRef={mockCalendarRef} />, {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
tokens: {
|
||||
accessToken: "token",
|
||||
},
|
||||
accessToken: 'token'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'667037022b752d0026472254/cal1': {
|
||||
name: 'Calendar 1',
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: "event1",
|
||||
id: 'event1',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
uid: 'event1',
|
||||
start: new Date().toISOString(),
|
||||
end: new Date(Date.now() + 3600000).toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
class: "PUBLIC",
|
||||
title: "Public Event",
|
||||
},
|
||||
},
|
||||
},
|
||||
class: 'PUBLIC',
|
||||
title: 'Public Event'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
});
|
||||
pending: false
|
||||
}
|
||||
})
|
||||
|
||||
expect(screen.getByText("Public Event")).toBeInTheDocument();
|
||||
});
|
||||
describe("BUGFIX", () => {
|
||||
expect(screen.getByText('Public Event')).toBeInTheDocument()
|
||||
})
|
||||
describe('BUGFIX', () => {
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
tokens: {
|
||||
accessToken: "token",
|
||||
},
|
||||
accessToken: 'token'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
color: { light: "#FFFFFF", dark: "#000000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'667037022b752d0026472254/cal1': {
|
||||
name: 'Calendar 1',
|
||||
id: '667037022b752d0026472254/cal1',
|
||||
color: { light: '#FFFFFF', dark: '#000000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
uid: "event1",
|
||||
title: "Original Event",
|
||||
start: new Date("2025-11-14T10:31:00.000Z").toISOString(),
|
||||
end: new Date("2025-11-14T11:31:00.000Z").toISOString(),
|
||||
class: "PUBLIC",
|
||||
partstat: "ACCEPTED",
|
||||
id: 'event1',
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Original Event',
|
||||
start: new Date('2025-11-14T10:31:00.000Z').toISOString(),
|
||||
end: new Date('2025-11-14T11:31:00.000Z').toISOString(),
|
||||
class: 'PUBLIC',
|
||||
partstat: 'ACCEPTED',
|
||||
sequence: 2,
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "CHAIR",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'CHAIR',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
{
|
||||
cn: "Bob",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "bob@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cn: 'Bob',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'bob@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
it("keeps all attendees event participation on title update", async () => {
|
||||
it('keeps all attendees event participation on title update', async () => {
|
||||
const updateSpy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const onClose = jest.fn();
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
const onClose = jest.fn()
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
eventId="event1"
|
||||
@@ -354,56 +344,56 @@ describe("CalendarApp integration", () => {
|
||||
typeOfAction="solo"
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
const titleInput = await screen.findByDisplayValue("Original Event");
|
||||
const titleInput = await screen.findByDisplayValue('Original Event')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(titleInput, "Updated Event");
|
||||
});
|
||||
fireEvent.change(titleInput, 'Updated Event')
|
||||
})
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
const saveButton = screen.getByRole('button', { name: /save/i })
|
||||
await act(async () => {
|
||||
saveButton.click();
|
||||
});
|
||||
saveButton.click()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(updateSpy).toHaveBeenCalled());
|
||||
await waitFor(() => expect(updateSpy).toHaveBeenCalled())
|
||||
|
||||
const dispatchedCalls = updateSpy.mock.calls;
|
||||
expect(dispatchedCalls.length).toBeGreaterThan(0);
|
||||
const updatedEvent = dispatchedCalls[0][0].newEvent;
|
||||
const dispatchedCalls = updateSpy.mock.calls
|
||||
expect(dispatchedCalls.length).toBeGreaterThan(0)
|
||||
const updatedEvent = dispatchedCalls[0][0].newEvent
|
||||
|
||||
// Ensure organizer attendee info is preserved
|
||||
const organizerAttendee = updatedEvent?.attendee?.find(
|
||||
(a: any) => a.cal_address === "alice@example.com"
|
||||
);
|
||||
(a: any) => a.cal_address === 'alice@example.com'
|
||||
)
|
||||
|
||||
expect(organizerAttendee).toBeTruthy();
|
||||
expect(organizerAttendee?.partstat).toBe("ACCEPTED");
|
||||
expect(organizerAttendee?.role).toBe("CHAIR");
|
||||
expect(organizerAttendee).toBeTruthy()
|
||||
expect(organizerAttendee?.partstat).toBe('ACCEPTED')
|
||||
expect(organizerAttendee?.role).toBe('CHAIR')
|
||||
|
||||
// Ensure normal attendee info is preserved too
|
||||
const normalAttendee = updatedEvent?.attendee?.find(
|
||||
(a: any) => a.cal_address === "bob@example.com"
|
||||
);
|
||||
(a: any) => a.cal_address === 'bob@example.com'
|
||||
)
|
||||
|
||||
expect(normalAttendee).toBeTruthy();
|
||||
expect(normalAttendee?.partstat).toBe("ACCEPTED");
|
||||
expect(normalAttendee?.role).toBe("REQ-PARTICIPANT");
|
||||
expect(normalAttendee).toBeTruthy()
|
||||
expect(normalAttendee?.partstat).toBe('ACCEPTED')
|
||||
expect(normalAttendee?.role).toBe('REQ-PARTICIPANT')
|
||||
|
||||
// Verify SEQUENCE is incremented
|
||||
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||
});
|
||||
expect(updatedEvent?.sequence).toBe(3) // 2 + 1
|
||||
})
|
||||
|
||||
it("changes normal attendee to need action on time update and no organizer changes", async () => {
|
||||
it('changes normal attendee to need action on time update and no organizer changes', async () => {
|
||||
const updateSpy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const onClose = jest.fn();
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
const onClose = jest.fn()
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
eventId="event1"
|
||||
@@ -413,78 +403,78 @@ describe("CalendarApp integration", () => {
|
||||
typeOfAction="solo"
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
const startDateInput = await screen.findByTestId("start-time-input");
|
||||
screen.getByRole('button', { name: 'common.moreOptions' })
|
||||
)
|
||||
const startDateInput = await screen.findByTestId('start-time-input')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(startDateInput, {
|
||||
target: { value: "08:00" },
|
||||
});
|
||||
fireEvent.blur(startDateInput);
|
||||
});
|
||||
target: { value: '08:00' }
|
||||
})
|
||||
fireEvent.blur(startDateInput)
|
||||
})
|
||||
|
||||
// Wait for blur handler to complete
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
})
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
const saveButton = screen.getByRole('button', { name: /save/i })
|
||||
await act(async () => {
|
||||
saveButton.click();
|
||||
});
|
||||
saveButton.click()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(updateSpy).toHaveBeenCalled());
|
||||
await waitFor(() => expect(updateSpy).toHaveBeenCalled())
|
||||
|
||||
const dispatchedCalls = updateSpy.mock.calls;
|
||||
expect(dispatchedCalls.length).toBeGreaterThan(0);
|
||||
const updatedEvent = dispatchedCalls[0][0].newEvent;
|
||||
const dispatchedCalls = updateSpy.mock.calls
|
||||
expect(dispatchedCalls.length).toBeGreaterThan(0)
|
||||
const updatedEvent = dispatchedCalls[0][0].newEvent
|
||||
|
||||
// Ensure organizer attendee info is preserved
|
||||
const organizerAttendee = updatedEvent?.attendee?.find(
|
||||
(a: any) => a.cal_address === "alice@example.com"
|
||||
);
|
||||
(a: any) => a.cal_address === 'alice@example.com'
|
||||
)
|
||||
|
||||
expect(organizerAttendee).toBeTruthy();
|
||||
expect(organizerAttendee?.partstat).toBe("ACCEPTED");
|
||||
expect(organizerAttendee?.role).toBe("CHAIR");
|
||||
expect(organizerAttendee).toBeTruthy()
|
||||
expect(organizerAttendee?.partstat).toBe('ACCEPTED')
|
||||
expect(organizerAttendee?.role).toBe('CHAIR')
|
||||
|
||||
// Ensure normal attendee partstat is updated
|
||||
const normalAttendee = updatedEvent?.attendee?.find(
|
||||
(a: any) => a.cal_address === "bob@example.com"
|
||||
);
|
||||
(a: any) => a.cal_address === 'bob@example.com'
|
||||
)
|
||||
|
||||
expect(normalAttendee).toBeTruthy();
|
||||
expect(normalAttendee?.partstat).toBe("NEEDS-ACTION");
|
||||
expect(normalAttendee?.role).toBe("REQ-PARTICIPANT");
|
||||
expect(normalAttendee).toBeTruthy()
|
||||
expect(normalAttendee?.partstat).toBe('NEEDS-ACTION')
|
||||
expect(normalAttendee?.role).toBe('REQ-PARTICIPANT')
|
||||
|
||||
// Verify SEQUENCE is incremented
|
||||
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||
});
|
||||
expect(updatedEvent?.sequence).toBe(3) // 2 + 1
|
||||
})
|
||||
|
||||
it("update event attendees on drag", async () => {
|
||||
it('update event attendees on drag', async () => {
|
||||
// Mock dispatch locally — this test calls createEventHandlers directly
|
||||
// and does not go through the Redux store or useAppDispatch.
|
||||
const mockDispatch = jest.fn().mockReturnValue(
|
||||
Object.assign(Promise.resolve({}), {
|
||||
unwrap: () => Promise.resolve({}),
|
||||
unwrap: () => Promise.resolve({})
|
||||
})
|
||||
) as unknown as AppDispatch;
|
||||
) as unknown as AppDispatch
|
||||
jest
|
||||
.spyOn(appHooks, "useAppDispatch")
|
||||
.mockReturnValue(mockDispatch as unknown as AppDispatch);
|
||||
.spyOn(appHooks, 'useAppDispatch')
|
||||
.mockReturnValue(mockDispatch as unknown as AppDispatch)
|
||||
|
||||
const updateSpy = 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 eventHandlers = createEventHandlers({
|
||||
setSelectedRange: jest.fn(),
|
||||
@@ -499,70 +489,70 @@ describe("CalendarApp integration", () => {
|
||||
calendars: preloadedState.calendars.list,
|
||||
setSelectedEvent: jest.fn(),
|
||||
setAfterChoiceFunc: jest.fn(),
|
||||
setOpenEditModePopup: jest.fn(),
|
||||
} as unknown as EventHandlersProps);
|
||||
setOpenEditModePopup: jest.fn()
|
||||
} as unknown as EventHandlersProps)
|
||||
|
||||
const mockArg = {
|
||||
event: {
|
||||
_def: {
|
||||
extendedProps: {
|
||||
uid: "event1",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
},
|
||||
},
|
||||
uid: 'event1',
|
||||
calId: '667037022b752d0026472254/cal1'
|
||||
}
|
||||
}
|
||||
},
|
||||
// drag event → move by 1 day
|
||||
delta: { years: 0, months: 0, days: 1, milliseconds: 0 },
|
||||
};
|
||||
delta: { years: 0, months: 0, days: 1, milliseconds: 0 }
|
||||
}
|
||||
|
||||
renderCalendar();
|
||||
eventHandlers.handleEventDrop(mockArg);
|
||||
renderCalendar()
|
||||
eventHandlers.handleEventDrop(mockArg)
|
||||
|
||||
expect(updateSpy).toHaveBeenCalled();
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
|
||||
// Extract the dispatched update event
|
||||
const dispatched = updateSpy.mock.calls[0];
|
||||
const dispatched = updateSpy.mock.calls[0]
|
||||
|
||||
expect(dispatched).toBeTruthy();
|
||||
expect(dispatched).toBeTruthy()
|
||||
|
||||
const updatePayload = dispatched[0];
|
||||
const updatedEvent = updatePayload.newEvent;
|
||||
const updatePayload = dispatched[0]
|
||||
const updatedEvent = updatePayload.newEvent
|
||||
|
||||
// Organizer should remain unchanged
|
||||
const organizer = updatedEvent.attendee.find(
|
||||
(a: any) => a.cal_address === "alice@example.com"
|
||||
);
|
||||
expect(organizer?.partstat).toBe("ACCEPTED");
|
||||
(a: any) => a.cal_address === 'alice@example.com'
|
||||
)
|
||||
expect(organizer?.partstat).toBe('ACCEPTED')
|
||||
|
||||
// Normal attendee must become NEEDS-ACTION
|
||||
const normal = updatedEvent.attendee.find(
|
||||
(a: any) => a.cal_address === "bob@example.com"
|
||||
);
|
||||
expect(normal?.partstat).toBe("NEEDS-ACTION");
|
||||
(a: any) => a.cal_address === 'bob@example.com'
|
||||
)
|
||||
expect(normal?.partstat).toBe('NEEDS-ACTION')
|
||||
|
||||
// Verify SEQUENCE is incremented
|
||||
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||
});
|
||||
expect(updatedEvent?.sequence).toBe(3) // 2 + 1
|
||||
})
|
||||
|
||||
it("update event attendees on resize", async () => {
|
||||
it('update event attendees on resize', async () => {
|
||||
// Mock dispatch locally — this test calls createEventHandlers directly
|
||||
// and does not go through the Redux store or useAppDispatch.
|
||||
const mockDispatch = jest.fn().mockReturnValue(
|
||||
Object.assign(Promise.resolve({}), {
|
||||
unwrap: () => Promise.resolve({}),
|
||||
unwrap: () => Promise.resolve({})
|
||||
})
|
||||
) as unknown as AppDispatch;
|
||||
) as unknown as AppDispatch
|
||||
jest
|
||||
.spyOn(appHooks, "useAppDispatch")
|
||||
.mockReturnValue(mockDispatch as unknown as AppDispatch);
|
||||
.spyOn(appHooks, 'useAppDispatch')
|
||||
.mockReturnValue(mockDispatch as unknown as AppDispatch)
|
||||
|
||||
const updateSpy = 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 eventHandlers = createEventHandlers({
|
||||
setSelectedRange: jest.fn(),
|
||||
@@ -577,49 +567,49 @@ describe("CalendarApp integration", () => {
|
||||
calendars: preloadedState.calendars.list,
|
||||
setSelectedEvent: jest.fn(),
|
||||
setAfterChoiceFunc: jest.fn(),
|
||||
setOpenEditModePopup: jest.fn(),
|
||||
} as unknown as EventHandlersProps);
|
||||
setOpenEditModePopup: jest.fn()
|
||||
} as unknown as EventHandlersProps)
|
||||
|
||||
const mockArg = {
|
||||
event: {
|
||||
_def: {
|
||||
extendedProps: {
|
||||
uid: "event1",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
},
|
||||
},
|
||||
uid: 'event1',
|
||||
calId: '667037022b752d0026472254/cal1'
|
||||
}
|
||||
}
|
||||
},
|
||||
startDelta: { years: 0, months: 0, days: 0, milliseconds: 0 },
|
||||
endDelta: { years: 0, months: 0, days: 0, milliseconds: 3600000 }, // 1 hour
|
||||
};
|
||||
endDelta: { years: 0, months: 0, days: 0, milliseconds: 3600000 } // 1 hour
|
||||
}
|
||||
|
||||
renderCalendar();
|
||||
eventHandlers.handleEventResize(mockArg);
|
||||
renderCalendar()
|
||||
eventHandlers.handleEventResize(mockArg)
|
||||
|
||||
expect(updateSpy).toHaveBeenCalled();
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
|
||||
// Extract the dispatched update event
|
||||
const dispatched = updateSpy.mock.calls[0];
|
||||
const dispatched = updateSpy.mock.calls[0]
|
||||
|
||||
expect(dispatched).toBeTruthy();
|
||||
expect(dispatched).toBeTruthy()
|
||||
|
||||
const updatePayload = dispatched[0];
|
||||
const updatedEvent = updatePayload.newEvent;
|
||||
const updatePayload = dispatched[0]
|
||||
const updatedEvent = updatePayload.newEvent
|
||||
|
||||
// Organizer should remain unchanged
|
||||
const organizer = updatedEvent.attendee.find(
|
||||
(a: any) => a.cal_address === "alice@example.com"
|
||||
);
|
||||
expect(organizer?.partstat).toBe("ACCEPTED");
|
||||
(a: any) => a.cal_address === 'alice@example.com'
|
||||
)
|
||||
expect(organizer?.partstat).toBe('ACCEPTED')
|
||||
|
||||
// Normal attendee must become NEEDS-ACTION
|
||||
const normal = updatedEvent.attendee.find(
|
||||
(a: any) => a.cal_address === "bob@example.com"
|
||||
);
|
||||
expect(normal?.partstat).toBe("NEEDS-ACTION");
|
||||
(a: any) => a.cal_address === 'bob@example.com'
|
||||
)
|
||||
expect(normal?.partstat).toBe('NEEDS-ACTION')
|
||||
|
||||
// Verify SEQUENCE is incremented
|
||||
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(updatedEvent?.sequence).toBe(3) // 2 + 1
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +1,65 @@
|
||||
import * as appHooks from "@/app/hooks";
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import CalendarApp from "@/components/Calendar/Calendar";
|
||||
import { jest } from "@jest/globals";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import * as appHooks from '@/app/hooks'
|
||||
import { AppDispatch } from '@/app/store'
|
||||
import CalendarApp from '@/components/Calendar/Calendar'
|
||||
import { jest } from '@jest/globals'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
describe("MiniCalendar", () => {
|
||||
const day = new Date();
|
||||
describe('MiniCalendar', () => {
|
||||
const day = new Date()
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
const dispatch = jest.fn().mockReturnValue(
|
||||
Object.assign(Promise.resolve({}), {
|
||||
unwrap: () => Promise.resolve({}),
|
||||
unwrap: () => Promise.resolve({})
|
||||
})
|
||||
) as unknown as AppDispatch;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
jest.useFakeTimers().clearAllTimers();
|
||||
});
|
||||
) as unknown as AppDispatch
|
||||
jest.spyOn(appHooks, 'useAppDispatch').mockReturnValue(dispatch)
|
||||
jest.useFakeTimers().clearAllTimers()
|
||||
})
|
||||
|
||||
const renderCalendar = () => {
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
},
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
name: "Calendar 1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["test@test.com"] },
|
||||
'667037022b752d0026472254/cal1': {
|
||||
name: 'Calendar 1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['test@test.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
id: "event1",
|
||||
title: "Test Event",
|
||||
start: day.toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
calId: '667037022b752d0026472254/cal1',
|
||||
id: 'event1',
|
||||
title: 'Test Event',
|
||||
start: day.toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
const mockCalendarRef = { current: null };
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
const mockCalendarRef = { current: null }
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
it("renders mini calendar with today in orange", async () => {
|
||||
renderCalendar();
|
||||
const today = new Date();
|
||||
const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
|
||||
it('renders mini calendar with today in orange', async () => {
|
||||
renderCalendar()
|
||||
const today = new Date()
|
||||
const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`
|
||||
|
||||
const todayTile = screen.getByTestId(dateTestId);
|
||||
expect(todayTile).toHaveClass("today");
|
||||
});
|
||||
});
|
||||
const todayTiles = screen.getAllByTestId(dateTestId)
|
||||
expect(todayTiles[0]).toHaveClass('today')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,412 +1,410 @@
|
||||
import { PeopleSearch, User } from "@/components/Attendees/PeopleSearch";
|
||||
import { searchUsers } from "@/features/User/userAPI";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import { PeopleSearch, User } from '@/components/Attendees/PeopleSearch'
|
||||
import { searchUsers } from '@/features/User/userAPI'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/features/User/userAPI");
|
||||
const mockedSearchUsers = searchUsers as jest.MockedFunction<
|
||||
typeof searchUsers
|
||||
>;
|
||||
jest.mock('@/features/User/userAPI')
|
||||
const mockedSearchUsers = searchUsers as jest.MockedFunction<typeof searchUsers>
|
||||
|
||||
describe("PeopleSearch", () => {
|
||||
describe('PeopleSearch', () => {
|
||||
const baseUser: User = {
|
||||
email: "test@example.com",
|
||||
displayName: "Test User",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
openpaasId: "1234567890",
|
||||
};
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
avatarUrl: 'https://example.com/avatar.png',
|
||||
openpaasId: '1234567890'
|
||||
}
|
||||
|
||||
function setup(
|
||||
selectedUsers: User[] = [],
|
||||
props?: Partial<React.ComponentProps<typeof PeopleSearch>>
|
||||
) {
|
||||
const onChange = jest.fn();
|
||||
const onChange = jest.fn()
|
||||
renderWithProviders(
|
||||
<PeopleSearch
|
||||
objectTypes={["user"]}
|
||||
objectTypes={['user']}
|
||||
selectedUsers={selectedUsers}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return { onChange };
|
||||
)
|
||||
return { onChange }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
mockedSearchUsers.mockReset();
|
||||
});
|
||||
jest.useFakeTimers()
|
||||
mockedSearchUsers.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it("calls searchUsers after debounce when typing", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||
setup();
|
||||
it('calls searchUsers after debounce when typing', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser])
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Test')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
|
||||
});
|
||||
});
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith('Test', ['user'])
|
||||
})
|
||||
})
|
||||
|
||||
it("renders search results and allows selection", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||
const { onChange } = setup();
|
||||
it('renders search results and allows selection', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser])
|
||||
const { onChange } = setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Test')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
await userEvent.click(option);
|
||||
const option = await screen.findByText('Test User')
|
||||
await userEvent.click(option)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it("does not show already selected users in options", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||
setup([baseUser]);
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
it('does not show already selected users in options', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser])
|
||||
setup([baseUser])
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Test')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByText('test@example.com')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("triggers onToggleEventPreview on Enter key press", () => {
|
||||
const onToggleEventPreview = jest.fn();
|
||||
setup([], { onToggleEventPreview });
|
||||
it('triggers onToggleEventPreview on Enter key press', () => {
|
||||
const onToggleEventPreview = jest.fn()
|
||||
setup([], { onToggleEventPreview })
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
const input = screen.getByRole('combobox')
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
|
||||
expect(onToggleEventPreview).toHaveBeenCalled();
|
||||
});
|
||||
expect(onToggleEventPreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("respects disabled state", () => {
|
||||
setup([], { disabled: true });
|
||||
expect(screen.getByRole("combobox")).toBeDisabled();
|
||||
});
|
||||
it('respects disabled state', () => {
|
||||
setup([], { disabled: true })
|
||||
expect(screen.getByRole('combobox')).toBeDisabled()
|
||||
})
|
||||
|
||||
it("no options doesn't show dropdown when input is empty", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||
setup();
|
||||
const input = screen.getByRole("combobox");
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser])
|
||||
setup()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
userEvent.type(input, "Test");
|
||||
userEvent.type(input, 'Test')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
userEvent.clear(input);
|
||||
userEvent.clear(input)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("shows 'No results' when search succeeds but returns empty array", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([]);
|
||||
setup();
|
||||
mockedSearchUsers.mockResolvedValueOnce([])
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Test')
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
const noResults = await screen.findByText(
|
||||
"peopleSearch.noResults",
|
||||
'peopleSearch.noResults',
|
||||
{},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
expect(noResults).toBeInTheDocument();
|
||||
});
|
||||
)
|
||||
expect(noResults).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not clear options when search fails and shows error snackbar", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||
setup();
|
||||
it('does not clear options when search fails and shows error snackbar', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser])
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Test')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test User")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Test User')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
mockedSearchUsers.mockRejectedValueOnce(new Error("Network error"));
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "Error");
|
||||
mockedSearchUsers.mockRejectedValueOnce(new Error('Network error'))
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, 'Error')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
const errorMessage = await screen.findByText("peopleSearch.searchError");
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
const errorMessage = await screen.findByText('peopleSearch.searchError')
|
||||
expect(errorMessage).toBeInTheDocument()
|
||||
|
||||
expect(screen.queryByText("Test User")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Test User')).not.toBeInTheDocument()
|
||||
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "Test");
|
||||
mockedSearchUsers.mockResolvedValueOnce([baseUser])
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, 'Test')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test User")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText('Test User')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("shows loading text when searching", async () => {
|
||||
let resolveSearch: (value: User[]) => void;
|
||||
const searchPromise = new Promise<User[]>((resolve) => {
|
||||
resolveSearch = resolve;
|
||||
});
|
||||
mockedSearchUsers.mockReturnValueOnce(searchPromise);
|
||||
setup();
|
||||
it('shows loading text when searching', async () => {
|
||||
let resolveSearch: (value: User[]) => void
|
||||
const searchPromise = new Promise<User[]>(resolve => {
|
||||
resolveSearch = resolve
|
||||
})
|
||||
mockedSearchUsers.mockReturnValueOnce(searchPromise)
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Test')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
const loadingText = await screen.findByText(
|
||||
"peopleSearch.loading",
|
||||
'peopleSearch.loading',
|
||||
{},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
expect(loadingText).toBeInTheDocument();
|
||||
)
|
||||
expect(loadingText).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
resolveSearch!([baseUser]);
|
||||
await searchPromise;
|
||||
});
|
||||
});
|
||||
resolveSearch!([baseUser])
|
||||
await searchPromise
|
||||
})
|
||||
})
|
||||
|
||||
describe("paste multiple attendees", () => {
|
||||
describe('paste multiple attendees', () => {
|
||||
function setupFreeSolo(selectedUsers: User[] = []) {
|
||||
const onChange = jest.fn();
|
||||
const onChange = jest.fn()
|
||||
renderWithProviders(
|
||||
<PeopleSearch
|
||||
objectTypes={["user"]}
|
||||
objectTypes={['user']}
|
||||
selectedUsers={selectedUsers}
|
||||
onChange={onChange}
|
||||
freeSolo
|
||||
/>
|
||||
);
|
||||
return { onChange };
|
||||
)
|
||||
return { onChange }
|
||||
}
|
||||
|
||||
it("splits pasted comma-separated emails into individual attendees", async () => {
|
||||
const { onChange } = setupFreeSolo();
|
||||
const input = screen.getByRole("combobox");
|
||||
it('splits pasted comma-separated emails into individual attendees', async () => {
|
||||
const { onChange } = setupFreeSolo()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => "alice@example.com, bob@example.com",
|
||||
},
|
||||
});
|
||||
getData: () => 'alice@example.com, bob@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ email: "alice@example.com" }),
|
||||
expect.objectContaining({ email: "bob@example.com" }),
|
||||
expect.objectContaining({ email: 'alice@example.com' }),
|
||||
expect.objectContaining({ email: 'bob@example.com' })
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("splits pasted semicolon-separated emails", async () => {
|
||||
const { onChange } = setupFreeSolo();
|
||||
const input = screen.getByRole("combobox");
|
||||
it('splits pasted semicolon-separated emails', async () => {
|
||||
const { onChange } = setupFreeSolo()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => "alice@example.com;bob@example.com",
|
||||
},
|
||||
});
|
||||
getData: () => 'alice@example.com;bob@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ email: "alice@example.com" }),
|
||||
expect.objectContaining({ email: "bob@example.com" }),
|
||||
expect.objectContaining({ email: 'alice@example.com' }),
|
||||
expect.objectContaining({ email: 'bob@example.com' })
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("splits pasted newline-separated emails", async () => {
|
||||
const { onChange } = setupFreeSolo();
|
||||
const input = screen.getByRole("combobox");
|
||||
it('splits pasted newline-separated emails', async () => {
|
||||
const { onChange } = setupFreeSolo()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => "alice@example.com\nbob@example.com",
|
||||
},
|
||||
});
|
||||
getData: () => 'alice@example.com\nbob@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ email: "alice@example.com" }),
|
||||
expect.objectContaining({ email: "bob@example.com" }),
|
||||
expect.objectContaining({ email: 'alice@example.com' }),
|
||||
expect.objectContaining({ email: 'bob@example.com' })
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("splits pasted space-separated emails", async () => {
|
||||
const { onChange } = setupFreeSolo();
|
||||
const input = screen.getByRole("combobox");
|
||||
it('splits pasted space-separated emails', async () => {
|
||||
const { onChange } = setupFreeSolo()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => "alice@example.com bob@example.com",
|
||||
},
|
||||
});
|
||||
getData: () => 'alice@example.com bob@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ email: "alice@example.com" }),
|
||||
expect.objectContaining({ email: "bob@example.com" }),
|
||||
expect.objectContaining({ email: 'alice@example.com' }),
|
||||
expect.objectContaining({ email: 'bob@example.com' })
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("skips duplicate emails already in selectedUsers", async () => {
|
||||
it('skips duplicate emails already in selectedUsers', async () => {
|
||||
const existing: User = {
|
||||
email: "alice@example.com",
|
||||
displayName: "Alice",
|
||||
};
|
||||
const { onChange } = setupFreeSolo([existing]);
|
||||
const input = screen.getByRole("combobox");
|
||||
email: 'alice@example.com',
|
||||
displayName: 'Alice'
|
||||
}
|
||||
const { onChange } = setupFreeSolo([existing])
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => "alice@example.com, bob@example.com",
|
||||
},
|
||||
});
|
||||
getData: () => 'alice@example.com, bob@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
existing,
|
||||
expect.objectContaining({ email: "bob@example.com" }),
|
||||
expect.objectContaining({ email: 'bob@example.com' })
|
||||
])
|
||||
);
|
||||
)
|
||||
// Should NOT have alice duplicated
|
||||
const call = onChange.mock.calls[0][1];
|
||||
const call = onChange.mock.calls[0][1]
|
||||
const aliceCount = call.filter(
|
||||
(u: User) => u.email === "alice@example.com"
|
||||
).length;
|
||||
expect(aliceCount).toBe(1);
|
||||
});
|
||||
});
|
||||
(u: User) => u.email === 'alice@example.com'
|
||||
).length
|
||||
expect(aliceCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
it("leaves invalid text in input when some emails are invalid", async () => {
|
||||
const { onChange } = setupFreeSolo();
|
||||
const input = screen.getByRole("combobox");
|
||||
it('leaves invalid text in input when some emails are invalid', async () => {
|
||||
const { onChange } = setupFreeSolo()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => "alice@example.com, not-an-email, bob@example.com",
|
||||
},
|
||||
});
|
||||
getData: () => 'alice@example.com, not-an-email, bob@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ email: "alice@example.com" }),
|
||||
expect.objectContaining({ email: "bob@example.com" }),
|
||||
expect.objectContaining({ email: 'alice@example.com' }),
|
||||
expect.objectContaining({ email: 'bob@example.com' })
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("does not intercept paste of a single email", async () => {
|
||||
const { onChange } = setupFreeSolo();
|
||||
const input = screen.getByRole("combobox");
|
||||
it('does not intercept paste of a single email', async () => {
|
||||
const { onChange } = setupFreeSolo()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => "alice@example.com",
|
||||
},
|
||||
});
|
||||
getData: () => 'alice@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
// Single email should NOT trigger multi-paste handler
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it("retains input value when field loses focus (blur)", async () => {
|
||||
let resolveSearch: (value: User[]) => void;
|
||||
const searchPromise = new Promise<User[]>((resolve) => {
|
||||
resolveSearch = resolve;
|
||||
});
|
||||
mockedSearchUsers.mockReturnValueOnce(searchPromise);
|
||||
it('retains input value when field loses focus (blur)', async () => {
|
||||
let resolveSearch: (value: User[]) => void
|
||||
const searchPromise = new Promise<User[]>(resolve => {
|
||||
resolveSearch = resolve
|
||||
})
|
||||
mockedSearchUsers.mockReturnValueOnce(searchPromise)
|
||||
await act(async () => {
|
||||
setup();
|
||||
});
|
||||
setup()
|
||||
})
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(input, "Test");
|
||||
});
|
||||
userEvent.type(input, 'Test')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
|
||||
});
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith('Test', ['user'])
|
||||
})
|
||||
|
||||
expect(input).toHaveValue("Test");
|
||||
expect(input).toHaveValue('Test')
|
||||
|
||||
await act(async () => {
|
||||
input.blur();
|
||||
});
|
||||
input.blur()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input).toHaveValue("Test");
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(input).toHaveValue('Test')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
import { RootState } from "@/app/store";
|
||||
import RepeatEvent from "@/components/Event/EventRepeat";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventPopover from "@/features/Events/EventModal";
|
||||
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
||||
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import { RootState } from '@/app/store'
|
||||
import RepeatEvent from '@/components/Event/EventRepeat'
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import EventPopover from '@/features/Events/EventModal'
|
||||
import { RepetitionObject } from '@/features/Events/EventsTypes'
|
||||
import { CalendarApi, DateSelectArg } from '@fullcalendar/core'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
const baseRepetition: RepetitionObject = {
|
||||
freq: "",
|
||||
freq: '',
|
||||
interval: 1,
|
||||
occurrences: 0,
|
||||
endDate: "",
|
||||
};
|
||||
endDate: ''
|
||||
}
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
const mockSetSelectedRange = jest.fn();
|
||||
const mockOnClose = jest.fn()
|
||||
const mockSetSelectedRange = jest.fn()
|
||||
const mockCalendarRef = {
|
||||
current: { select: jest.fn() } as unknown as CalendarApi,
|
||||
};
|
||||
current: { select: jest.fn() } as unknown as CalendarApi
|
||||
}
|
||||
|
||||
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 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
|
||||
|
||||
function setupRepeatEvent(
|
||||
props?: Partial<RepetitionObject>,
|
||||
state?: RootState
|
||||
) {
|
||||
const setRepetition = jest.fn();
|
||||
const setRepetition = jest.fn()
|
||||
renderWithProviders(
|
||||
<RepeatEvent
|
||||
repetition={{ ...baseRepetition, ...props }}
|
||||
@@ -72,20 +72,20 @@ function setupRepeatEvent(
|
||||
isOwn={true}
|
||||
/>,
|
||||
state
|
||||
);
|
||||
return { setRepetition };
|
||||
)
|
||||
return { setRepetition }
|
||||
}
|
||||
|
||||
async function setupEventPopover() {
|
||||
jest
|
||||
.spyOn(crypto, "randomUUID")
|
||||
.mockReturnValue("fixed-uuid-with-correct-format");
|
||||
.spyOn(crypto, 'randomUUID')
|
||||
.mockReturnValue('fixed-uuid-with-correct-format')
|
||||
const originalDateResolvedOptions =
|
||||
new Intl.DateTimeFormat().resolvedOptions();
|
||||
jest.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockReturnValue({
|
||||
new Intl.DateTimeFormat().resolvedOptions()
|
||||
jest.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockReturnValue({
|
||||
...originalDateResolvedOptions,
|
||||
timeZone: "UTC",
|
||||
});
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
|
||||
renderWithProviders(
|
||||
<EventPopover
|
||||
@@ -97,324 +97,320 @@ async function setupEventPopover() {
|
||||
calendarRef={mockCalendarRef}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
)
|
||||
|
||||
// Fill in title
|
||||
const titleInput = screen.getByLabelText("event.form.title");
|
||||
fireEvent.change(titleInput, { target: { value: "Meeting" } });
|
||||
const titleInput = screen.getByLabelText('event.form.title')
|
||||
fireEvent.change(titleInput, { target: { value: 'Meeting' } })
|
||||
|
||||
// Click More options to expand the dialog
|
||||
const showMoreButton = screen.getByRole("button", {
|
||||
name: "common.moreOptions",
|
||||
});
|
||||
fireEvent.click(showMoreButton);
|
||||
const showMoreButton = screen.getByRole('button', {
|
||||
name: 'common.moreOptions'
|
||||
})
|
||||
fireEvent.click(showMoreButton)
|
||||
|
||||
// Check Repeat checkbox to show repeat options
|
||||
const repeatCheckbox = screen.getByLabelText("event.form.repeat");
|
||||
fireEvent.click(repeatCheckbox);
|
||||
const repeatCheckbox = screen.getByLabelText('event.form.repeat')
|
||||
fireEvent.click(repeatCheckbox)
|
||||
|
||||
// Wait for RepeatEvent component to be rendered
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("event.repeat.frequency.weeks")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('event.repeat.frequency.weeks')).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
|
||||
async function expectRRule(expected: Partial<RepetitionObject>) {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
act(() => fireEvent.click(saveButton));
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled());
|
||||
.spyOn(eventThunks, 'putEventAsync')
|
||||
.mockImplementation(payload => {
|
||||
const promise = Promise.resolve(payload)
|
||||
;(promise as any).unwrap = () => promise
|
||||
return () => promise as any
|
||||
})
|
||||
const saveButton = screen.getByRole('button', { name: /save/i })
|
||||
act(() => fireEvent.click(saveButton))
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled())
|
||||
|
||||
const received = spy.mock.calls[0][0];
|
||||
expect(received.newEvent.repetition).toMatchObject(expected);
|
||||
const received = spy.mock.calls[0][0]
|
||||
expect(received.newEvent.repetition).toMatchObject(expected)
|
||||
}
|
||||
|
||||
describe("RepeatEvent Component", () => {
|
||||
describe('RepeatEvent Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("renders with no repetition by default", () => {
|
||||
setupRepeatEvent();
|
||||
it('renders with no repetition by default', () => {
|
||||
setupRepeatEvent()
|
||||
|
||||
// Check that interval input shows default value
|
||||
const intervalInput = screen.getByTestId("repeat-interval");
|
||||
expect(intervalInput).toBeInTheDocument();
|
||||
const intervalInput = screen.getByTestId('repeat-interval')
|
||||
expect(intervalInput).toBeInTheDocument()
|
||||
|
||||
// Check that frequency dropdown shows default value
|
||||
const frequencySelect = screen.getByRole("combobox");
|
||||
expect(frequencySelect).toBeInTheDocument();
|
||||
});
|
||||
const frequencySelect = screen.getByRole('combobox')
|
||||
expect(frequencySelect).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("allows selecting repetition frequency", () => {
|
||||
const { setRepetition } = setupRepeatEvent();
|
||||
it('allows selecting repetition frequency', () => {
|
||||
const { setRepetition } = setupRepeatEvent()
|
||||
|
||||
// Click on frequency dropdown
|
||||
const frequencySelect = screen.getByRole("combobox");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const frequencySelect = screen.getByRole('combobox')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
|
||||
// Select Week(s)
|
||||
const weeklyOption = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.click(weeklyOption);
|
||||
const weeklyOption = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.click(weeklyOption)
|
||||
|
||||
expect(setRepetition).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ freq: "weekly" })
|
||||
);
|
||||
});
|
||||
expect.objectContaining({ freq: 'weekly' })
|
||||
)
|
||||
})
|
||||
|
||||
it("renders interval input when frequency is selected", () => {
|
||||
setupRepeatEvent({ freq: "daily" });
|
||||
it('renders interval input when frequency is selected', () => {
|
||||
setupRepeatEvent({ freq: 'daily' })
|
||||
|
||||
const intervalInput = screen.getByTestId("repeat-interval");
|
||||
expect(intervalInput).toBeInTheDocument();
|
||||
});
|
||||
const intervalInput = screen.getByTestId('repeat-interval')
|
||||
expect(intervalInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("updates interval value", () => {
|
||||
const { setRepetition } = setupRepeatEvent();
|
||||
it('updates interval value', () => {
|
||||
const { setRepetition } = setupRepeatEvent()
|
||||
|
||||
const intervalInput = screen.getByTestId("repeat-interval");
|
||||
fireEvent.change(intervalInput, { target: { value: "3" } });
|
||||
const intervalInput = screen.getByTestId('repeat-interval')
|
||||
fireEvent.change(intervalInput, { target: { value: '3' } })
|
||||
|
||||
expect(setRepetition).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ interval: 3 })
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("toggles day selection for weekly frequency", () => {
|
||||
const { setRepetition } = setupRepeatEvent({ freq: "weekly" });
|
||||
it('toggles day selection for weekly frequency', () => {
|
||||
const { setRepetition } = setupRepeatEvent({ freq: 'weekly' })
|
||||
|
||||
const mondayChip = screen.getByLabelText("event.repeat.days.monday");
|
||||
fireEvent.click(mondayChip);
|
||||
const mondayChip = screen.getByLabelText('event.repeat.days.monday')
|
||||
fireEvent.click(mondayChip)
|
||||
|
||||
expect(setRepetition).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ byday: ["MO"] })
|
||||
);
|
||||
});
|
||||
});
|
||||
expect.objectContaining({ byday: ['MO'] })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Repeat Event Integration Tests", () => {
|
||||
describe('Repeat Event Integration Tests', () => {
|
||||
// Increase timeout for all tests in this describe block
|
||||
jest.setTimeout(30000);
|
||||
jest.setTimeout(30000)
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("sends correct CalendarEvent payload for daily repeat", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct CalendarEvent payload for daily repeat', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// When Repeat checkbox is checked, repetition is set to empty object
|
||||
// We need to set the frequency manually
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const dailyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.days",
|
||||
});
|
||||
fireEvent.click(dailyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const dailyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.days'
|
||||
})
|
||||
fireEvent.click(dailyOption)
|
||||
|
||||
await expectRRule({ freq: "daily", interval: 1 });
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
await expectRRule({ freq: 'daily', interval: 1 })
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat daily with 2 day interval", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat daily with 2 day interval', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Ensure frequency is daily
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const dailyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.days",
|
||||
});
|
||||
fireEvent.click(dailyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const dailyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.days'
|
||||
})
|
||||
fireEvent.click(dailyOption)
|
||||
|
||||
// Set interval to 2
|
||||
const intervalInput = screen.getByTestId("repeat-interval");
|
||||
fireEvent.change(intervalInput, { target: { value: "2" } });
|
||||
const intervalInput = screen.getByTestId('repeat-interval')
|
||||
fireEvent.change(intervalInput, { target: { value: '2' } })
|
||||
|
||||
await expectRRule({ freq: "daily", interval: 2 });
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
await expectRRule({ freq: 'daily', interval: 2 })
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat daily for 5 repetitions", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat daily for 5 repetitions', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Ensure frequency is daily
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const dailyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.days",
|
||||
});
|
||||
fireEvent.click(dailyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const dailyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.days'
|
||||
})
|
||||
fireEvent.click(dailyOption)
|
||||
|
||||
// Select "After" end option
|
||||
const afterRadio = screen.getByLabelText(/after/i);
|
||||
fireEvent.click(afterRadio);
|
||||
const afterRadio = screen.getByLabelText(/after/i)
|
||||
fireEvent.click(afterRadio)
|
||||
|
||||
// Set occurrences to 5
|
||||
const occurrencesInput = screen.getByTestId("occurrences-input");
|
||||
fireEvent.change(occurrencesInput, { target: { value: "5" } });
|
||||
const occurrencesInput = screen.getByTestId('occurrences-input')
|
||||
fireEvent.change(occurrencesInput, { target: { value: '5' } })
|
||||
|
||||
await expectRRule({ freq: "daily", interval: 1, occurrences: 5 });
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
await expectRRule({ freq: 'daily', interval: 1, occurrences: 5 })
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat daily until specific date", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat daily until specific date', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Ensure frequency is daily
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const dailyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.days",
|
||||
});
|
||||
fireEvent.click(dailyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const dailyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.days'
|
||||
})
|
||||
fireEvent.click(dailyOption)
|
||||
|
||||
// Select "On" end option
|
||||
const onRadio = screen
|
||||
.getAllByLabelText(/on/i)
|
||||
.find((el) => el.type === "radio");
|
||||
fireEvent.click(onRadio!);
|
||||
.find(el => el.type === 'radio')
|
||||
fireEvent.click(onRadio!)
|
||||
|
||||
// End date is set by UI to some valid date string (YYYY-MM-DD)
|
||||
await expectRRule({
|
||||
freq: "daily",
|
||||
freq: 'daily',
|
||||
interval: 1,
|
||||
endDate: expect.any(String),
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
endDate: expect.any(String)
|
||||
})
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat weekly on specific days", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat weekly on specific days', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Select Week(s) frequency
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const weeklyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.weeks",
|
||||
});
|
||||
fireEvent.click(weeklyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const weeklyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.weeks'
|
||||
})
|
||||
fireEvent.click(weeklyOption)
|
||||
|
||||
// Select Thursday
|
||||
const thursdayCheckbox = screen.getByLabelText(
|
||||
"event.repeat.days.thursday"
|
||||
);
|
||||
fireEvent.click(thursdayCheckbox);
|
||||
const thursdayCheckbox = screen.getByLabelText('event.repeat.days.thursday')
|
||||
fireEvent.click(thursdayCheckbox)
|
||||
|
||||
await expectRRule({
|
||||
freq: "weekly",
|
||||
freq: 'weekly',
|
||||
interval: 1,
|
||||
byday: ["FR", "TH"],
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
byday: ['FR', 'TH']
|
||||
})
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat weekly with 3 week interval", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat weekly with 3 week interval', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Select Week(s) frequency
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const weeklyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.weeks",
|
||||
});
|
||||
fireEvent.click(weeklyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const weeklyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.weeks'
|
||||
})
|
||||
fireEvent.click(weeklyOption)
|
||||
|
||||
// Set interval to 3
|
||||
const intervalInput = screen.getByTestId("repeat-interval");
|
||||
fireEvent.change(intervalInput, { target: { value: "3" } });
|
||||
const intervalInput = screen.getByTestId('repeat-interval')
|
||||
fireEvent.change(intervalInput, { target: { value: '3' } })
|
||||
|
||||
await expectRRule({ freq: "weekly", interval: 3 });
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
await expectRRule({ freq: 'weekly', interval: 3 })
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat monthly", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat monthly', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Select Month(s) frequency
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const monthlyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.months",
|
||||
});
|
||||
fireEvent.click(monthlyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const monthlyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.months'
|
||||
})
|
||||
fireEvent.click(monthlyOption)
|
||||
|
||||
await expectRRule({ freq: "monthly", interval: 1 });
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
await expectRRule({ freq: 'monthly', interval: 1 })
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat monthly and end after 5 occurrences", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat monthly and end after 5 occurrences', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Select Month(s) frequency
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const monthlyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.months",
|
||||
});
|
||||
fireEvent.click(monthlyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const monthlyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.months'
|
||||
})
|
||||
fireEvent.click(monthlyOption)
|
||||
|
||||
// Select "After" end option
|
||||
const afterRadio = screen.getByLabelText(/after/i);
|
||||
fireEvent.click(afterRadio);
|
||||
const afterRadio = screen.getByLabelText(/after/i)
|
||||
fireEvent.click(afterRadio)
|
||||
|
||||
// Set occurrences to 5
|
||||
const occurrencesInput = screen.getByTestId("occurrences-input");
|
||||
fireEvent.change(occurrencesInput, { target: { value: "5" } });
|
||||
const occurrencesInput = screen.getByTestId('occurrences-input')
|
||||
fireEvent.change(occurrencesInput, { target: { value: '5' } })
|
||||
|
||||
await expectRRule({ freq: "monthly", interval: 1, occurrences: 5 });
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
await expectRRule({ freq: 'monthly', interval: 1, occurrences: 5 })
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat yearly", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat yearly', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Select Year(s) frequency
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const yearlyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.years",
|
||||
});
|
||||
fireEvent.click(yearlyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const yearlyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.years'
|
||||
})
|
||||
fireEvent.click(yearlyOption)
|
||||
|
||||
await expectRRule({ freq: "yearly", interval: 1 });
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
await expectRRule({ freq: 'yearly', interval: 1 })
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("sends correct API payload for repeat yearly with end option changes", async () => {
|
||||
await setupEventPopover();
|
||||
it('sends correct API payload for repeat yearly with end option changes', async () => {
|
||||
await setupEventPopover()
|
||||
|
||||
// Select Year(s) frequency
|
||||
const frequencySelect = screen.getByText("event.repeat.frequency.weeks");
|
||||
fireEvent.mouseDown(frequencySelect);
|
||||
const yearlyOption = screen.getByRole("option", {
|
||||
name: "event.repeat.frequency.years",
|
||||
});
|
||||
fireEvent.click(yearlyOption);
|
||||
const frequencySelect = screen.getByText('event.repeat.frequency.weeks')
|
||||
fireEvent.mouseDown(frequencySelect)
|
||||
const yearlyOption = screen.getByRole('option', {
|
||||
name: 'event.repeat.frequency.years'
|
||||
})
|
||||
fireEvent.click(yearlyOption)
|
||||
|
||||
// First choose "After" with 5 occurrences
|
||||
const afterRadio = screen.getByLabelText(/after/i);
|
||||
fireEvent.click(afterRadio);
|
||||
const occurrencesInput = screen.getByTestId("occurrences-input");
|
||||
fireEvent.change(occurrencesInput, { target: { value: "5" } });
|
||||
const afterRadio = screen.getByLabelText(/after/i)
|
||||
fireEvent.click(afterRadio)
|
||||
const occurrencesInput = screen.getByTestId('occurrences-input')
|
||||
fireEvent.change(occurrencesInput, { target: { value: '5' } })
|
||||
|
||||
// Then change mind and choose "Never"
|
||||
const neverRadio = screen.getByLabelText(/never/i);
|
||||
fireEvent.click(neverRadio);
|
||||
const neverRadio = screen.getByLabelText(/never/i)
|
||||
fireEvent.click(neverRadio)
|
||||
|
||||
await expectRRule({
|
||||
freq: "yearly",
|
||||
interval: 1,
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
freq: 'yearly',
|
||||
interval: 1
|
||||
})
|
||||
expect(mockOnClose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,224 +1,219 @@
|
||||
import {
|
||||
ResourceSearch,
|
||||
Resource,
|
||||
} from "@/components/Attendees/ResourceSearch";
|
||||
import { searchUsers } from "@/features/User/userAPI";
|
||||
import { act, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||
import { ResourceSearch, Resource } from '@/components/Attendees/ResourceSearch'
|
||||
import { searchUsers } from '@/features/User/userAPI'
|
||||
import { act, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/features/User/userAPI");
|
||||
const mockedSearchUsers = searchUsers as jest.MockedFunction<
|
||||
typeof searchUsers
|
||||
>;
|
||||
jest.mock('@/features/User/userAPI')
|
||||
const mockedSearchUsers = searchUsers as jest.MockedFunction<typeof searchUsers>
|
||||
|
||||
describe("ResourceSearch", () => {
|
||||
describe('ResourceSearch', () => {
|
||||
const baseResource: Resource = {
|
||||
displayName: "Projector Room",
|
||||
};
|
||||
displayName: 'Projector Room'
|
||||
}
|
||||
|
||||
function setup(
|
||||
selectedResources: Resource[] = [],
|
||||
props?: Partial<React.ComponentProps<typeof ResourceSearch>>
|
||||
) {
|
||||
const onChange = jest.fn();
|
||||
const onChange = jest.fn()
|
||||
renderWithProviders(
|
||||
<ResourceSearch
|
||||
objectTypes={["resource"]}
|
||||
objectTypes={['resource']}
|
||||
selectedResources={selectedResources}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return { onChange };
|
||||
)
|
||||
return { onChange }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
mockedSearchUsers.mockReset();
|
||||
});
|
||||
jest.useFakeTimers()
|
||||
mockedSearchUsers.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it("calls searchUsers after debounce when typing", async () => {
|
||||
it('calls searchUsers after debounce when typing', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([
|
||||
baseResource,
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>);
|
||||
setup();
|
||||
baseResource
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>)
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Room");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Room')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith("Room", ["resource"]);
|
||||
});
|
||||
});
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith('Room', ['resource'])
|
||||
})
|
||||
})
|
||||
|
||||
it("renders search results and allows selection", async () => {
|
||||
it('renders search results and allows selection', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([
|
||||
baseResource,
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>);
|
||||
const { onChange } = setup();
|
||||
baseResource
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>)
|
||||
const { onChange } = setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Room");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Room')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
const option = await screen.findByText("Projector Room");
|
||||
await userEvent.click(option);
|
||||
const option = await screen.findByText('Projector Room')
|
||||
await userEvent.click(option)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it("does not show already selected resources in options", async () => {
|
||||
it('does not show already selected resources in options', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([
|
||||
baseResource,
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>);
|
||||
setup([baseResource]);
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Projector");
|
||||
baseResource
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>)
|
||||
setup([baseResource])
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Projector')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
// It shouldn't be in the dropdown options anymore
|
||||
const options = screen.queryAllByRole("option");
|
||||
const options = screen.queryAllByRole('option')
|
||||
expect(
|
||||
options.find((opt) => opt.textContent === "Projector Room")
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
options.find(opt => opt.textContent === 'Projector Room')
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it("respects disabled state", () => {
|
||||
setup([], { disabled: true });
|
||||
expect(screen.getByRole("combobox")).toBeDisabled();
|
||||
});
|
||||
it('respects disabled state', () => {
|
||||
setup([], { disabled: true })
|
||||
expect(screen.getByRole('combobox')).toBeDisabled()
|
||||
})
|
||||
|
||||
it("no options doesn't show dropdown when input is empty", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([
|
||||
baseResource,
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>);
|
||||
setup();
|
||||
const input = screen.getByRole("combobox");
|
||||
baseResource
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>)
|
||||
setup()
|
||||
const input = screen.getByRole('combobox')
|
||||
|
||||
await userEvent.type(input, "Room");
|
||||
await userEvent.type(input, 'Room')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await userEvent.clear(input);
|
||||
await userEvent.clear(input)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("shows 'No results' when search succeeds but returns empty array", async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([]);
|
||||
setup();
|
||||
mockedSearchUsers.mockResolvedValueOnce([])
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Room");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Room')
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
const noResults = await screen.findByText(
|
||||
"resourceSearch.noResults",
|
||||
'resourceSearch.noResults',
|
||||
{},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
expect(noResults).toBeInTheDocument();
|
||||
});
|
||||
)
|
||||
expect(noResults).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("clears options when search fails and shows error snackbar", async () => {
|
||||
it('clears options when search fails and shows error snackbar', async () => {
|
||||
mockedSearchUsers.mockResolvedValueOnce([
|
||||
baseResource,
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>);
|
||||
setup();
|
||||
baseResource
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>)
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Room");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Room')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Projector Room")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Projector Room')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
mockedSearchUsers.mockRejectedValueOnce(new Error("Network error"));
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "Error");
|
||||
mockedSearchUsers.mockRejectedValueOnce(new Error('Network error'))
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, 'Error')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
const errorMessage = await screen.findByText("resourceSearch.searchError");
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
const errorMessage = await screen.findByText('resourceSearch.searchError')
|
||||
expect(errorMessage).toBeInTheDocument()
|
||||
|
||||
expect(screen.queryByText("Projector Room")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Projector Room')).not.toBeInTheDocument()
|
||||
|
||||
mockedSearchUsers.mockResolvedValueOnce([
|
||||
baseResource,
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>);
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "Room");
|
||||
baseResource
|
||||
] as unknown as Awaited<ReturnType<typeof searchUsers>>)
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, 'Room')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve();
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Projector Room")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText('Projector Room')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("shows loading text when searching", async () => {
|
||||
let resolveSearch: (value: Resource[]) => void;
|
||||
const searchPromise = new Promise<Resource[]>((resolve) => {
|
||||
resolveSearch = resolve;
|
||||
});
|
||||
it('shows loading text when searching', async () => {
|
||||
let resolveSearch: (value: Resource[]) => void
|
||||
const searchPromise = new Promise<Resource[]>(resolve => {
|
||||
resolveSearch = resolve
|
||||
})
|
||||
mockedSearchUsers.mockReturnValueOnce(
|
||||
searchPromise as unknown as ReturnType<typeof searchUsers>
|
||||
);
|
||||
setup();
|
||||
)
|
||||
setup()
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Room");
|
||||
const input = screen.getByRole('combobox')
|
||||
await userEvent.type(input, 'Room')
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
const loadingText = await screen.findByText(
|
||||
"resourceSearch.loading",
|
||||
'resourceSearch.loading',
|
||||
{},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
expect(loadingText).toBeInTheDocument();
|
||||
)
|
||||
expect(loadingText).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
resolveSearch!([baseResource]);
|
||||
await searchPromise;
|
||||
});
|
||||
});
|
||||
});
|
||||
resolveSearch!([baseResource])
|
||||
await searchPromise
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { ResponsiveDialog } from "@/components/Dialog";
|
||||
import { Button, TextField, TwakeMuiThemeProvider } from "@linagora/twake-mui";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { ResponsiveDialog } from '@/components/Dialog'
|
||||
import { Button, TextField, TwakeMuiThemeProvider } from '@linagora/twake-mui'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
|
||||
describe("ResponsiveDialog", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
const mockOnExpandToggle = jest.fn();
|
||||
describe('ResponsiveDialog', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
const mockOnExpandToggle = jest.fn()
|
||||
|
||||
const renderWithTheme = (ui: React.ReactElement) => {
|
||||
return render(<TwakeMuiThemeProvider>{ui}</TwakeMuiThemeProvider>);
|
||||
};
|
||||
return render(<TwakeMuiThemeProvider>{ui}</TwakeMuiThemeProvider>)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnClose.mockClear();
|
||||
mockOnExpandToggle.mockClear();
|
||||
});
|
||||
mockOnClose.mockClear()
|
||||
mockOnExpandToggle.mockClear()
|
||||
})
|
||||
|
||||
it("renders in normal mode by default", () => {
|
||||
it('renders in normal mode by default', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog open={true} onClose={mockOnClose} title="Test Dialog">
|
||||
<TextField label="Name" />
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Test Dialog")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/name/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Test Dialog')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/name/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders title in normal mode", () => {
|
||||
it('renders title in normal mode', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -37,13 +37,13 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("My Title")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("show less")).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('My Title')).toBeInTheDocument()
|
||||
expect(screen.queryByLabelText('show less')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders back arrow in extended mode", () => {
|
||||
it('renders back arrow in extended mode', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -54,13 +54,13 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.queryByText("My Title")).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("show less")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('My Title')).not.toBeInTheDocument()
|
||||
expect(screen.getByLabelText('show less')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onExpandToggle when back arrow is clicked", () => {
|
||||
it('calls onExpandToggle when back arrow is clicked', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -71,15 +71,15 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
const backButton = screen.getByLabelText("show less");
|
||||
fireEvent.click(backButton);
|
||||
const backButton = screen.getByLabelText('show less')
|
||||
fireEvent.click(backButton)
|
||||
|
||||
expect(mockOnExpandToggle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mockOnExpandToggle).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("renders actions when provided", () => {
|
||||
it('renders actions when provided', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -89,38 +89,38 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Custom Action")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Custom Action')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render actions when not provided", () => {
|
||||
it('does not render actions when not provided', () => {
|
||||
const { container } = renderWithTheme(
|
||||
<ResponsiveDialog open={true} onClose={mockOnClose} title="Test">
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
const dialogActions = container.querySelector(".MuiDialogActions-root");
|
||||
expect(dialogActions).not.toBeInTheDocument();
|
||||
});
|
||||
const dialogActions = container.querySelector('.MuiDialogActions-root')
|
||||
expect(dialogActions).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onClose when backdrop is clicked", () => {
|
||||
it('calls onClose when backdrop is clicked', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog open={true} onClose={mockOnClose} title="Test">
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
const backdrop = document.querySelector(".MuiBackdrop-root");
|
||||
const backdrop = document.querySelector('.MuiBackdrop-root')
|
||||
if (backdrop) {
|
||||
fireEvent.click(backdrop);
|
||||
fireEvent.click(backdrop)
|
||||
}
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("applies custom normalMaxWidth", () => {
|
||||
it('applies custom normalMaxWidth', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -130,24 +130,24 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Normal Width Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Normal Width Content")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Normal Width Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("wraps children in Stack component", () => {
|
||||
it('wraps children in Stack component', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog open={true} onClose={mockOnClose} title="Test">
|
||||
<TextField label="Field 1" />
|
||||
<TextField label="Field 2" />
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByLabelText("Field 1")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Field 2")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByLabelText('Field 1')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Field 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("uses correct spacing in normal mode", () => {
|
||||
it('uses correct spacing in normal mode', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -158,12 +158,12 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Normal Spacing Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Normal Spacing Content")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Normal Spacing Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("uses correct spacing in extended mode", () => {
|
||||
it('uses correct spacing in extended mode', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -174,12 +174,12 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Extended Spacing Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Extended Spacing Content")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Extended Spacing Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("applies contentSx custom styles", () => {
|
||||
it('applies contentSx custom styles', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -189,28 +189,28 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Custom Styled Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Custom Styled Content")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Custom Styled Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("applies titleSx custom styles", () => {
|
||||
it('applies titleSx custom styles', () => {
|
||||
const { container } = renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
title="Test"
|
||||
titleSx={{ color: "red" }}
|
||||
titleSx={{ color: 'red' }}
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
const title = screen.getByText("Test");
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
const title = screen.getByText('Test')
|
||||
expect(title).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows dividers when dividers prop is true", () => {
|
||||
it('shows dividers when dividers prop is true', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -220,12 +220,12 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content with Dividers</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Content with Dividers")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Content with Dividers')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not show back arrow when onExpandToggle is not provided", () => {
|
||||
it('does not show back arrow when onExpandToggle is not provided', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -235,13 +235,13 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.queryByLabelText("show less")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByLabelText('show less')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("accepts custom headerHeight", () => {
|
||||
it('accepts custom headerHeight', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -252,12 +252,12 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Custom Header Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Custom Header Content")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Custom Header Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders with custom expandedContentMaxWidth", () => {
|
||||
it('renders with custom expandedContentMaxWidth', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -268,22 +268,22 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Wide Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Wide Content")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Wide Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render dialog content when open is false", () => {
|
||||
it('does not render dialog content when open is false', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog open={false} onClose={mockOnClose} title="Test">
|
||||
<div>Test Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.queryByText("Test Content")).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders correctly in extended mode", () => {
|
||||
it('renders correctly in extended mode', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -294,13 +294,13 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Extended Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Extended Content")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("show less")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Extended Content')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('show less')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders expand and close icons in normal mode when showHeaderActions is true", () => {
|
||||
it('renders expand and close icons in normal mode when showHeaderActions is true', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -312,13 +312,13 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByLabelText("expand")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("close")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByLabelText('expand')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('close')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render header icons when showHeaderActions is false", () => {
|
||||
it('does not render header icons when showHeaderActions is false', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -330,14 +330,14 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.queryByLabelText("expand")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("close")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByLabelText('expand')).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText('close')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onClose when close icon is clicked", () => {
|
||||
it('calls onClose when close icon is clicked', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -348,15 +348,15 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
const closeButton = screen.getByLabelText("close");
|
||||
fireEvent.click(closeButton);
|
||||
const closeButton = screen.getByLabelText('close')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("calls onExpandToggle when expand icon is clicked", () => {
|
||||
it('calls onExpandToggle when expand icon is clicked', () => {
|
||||
renderWithTheme(
|
||||
<ResponsiveDialog
|
||||
open={true}
|
||||
@@ -368,11 +368,11 @@ describe("ResponsiveDialog", () => {
|
||||
>
|
||||
<div>Content</div>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
|
||||
const expandButton = screen.getByLabelText("expand");
|
||||
fireEvent.click(expandButton);
|
||||
const expandButton = screen.getByLabelText('expand')
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
expect(mockOnExpandToggle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
expect(mockOnExpandToggle).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useUserSearch } from "@/components/Attendees/useUserSearch";
|
||||
import { searchUsers } from "@/features/User/userAPI";
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useUserSearch } from '@/components/Attendees/useUserSearch'
|
||||
import { searchUsers } from '@/features/User/userAPI'
|
||||
|
||||
jest.mock("@/features/User/userAPI", () => ({
|
||||
searchUsers: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/features/User/userAPI', () => ({
|
||||
searchUsers: jest.fn()
|
||||
}))
|
||||
|
||||
describe("useUserSearch", () => {
|
||||
const mockSearchUsers = searchUsers as jest.Mock;
|
||||
describe('useUserSearch', () => {
|
||||
const mockSearchUsers = searchUsers as jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.useFakeTimers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it("should initialize with default values", () => {
|
||||
it('should initialize with default values', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useUserSearch({ objectTypes: ["user"], errorMessage: "Error" })
|
||||
);
|
||||
useUserSearch({ objectTypes: ['user'], errorMessage: 'Error' })
|
||||
)
|
||||
|
||||
expect(result.current.query).toBe("");
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.options).toEqual([]);
|
||||
expect(result.current.hasSearched).toBe(false);
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(result.current.inputError).toBeNull();
|
||||
expect(result.current.snackbarOpen).toBe(false);
|
||||
expect(result.current.snackbarMessage).toBe("");
|
||||
});
|
||||
expect(result.current.query).toBe('')
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.options).toEqual([])
|
||||
expect(result.current.hasSearched).toBe(false)
|
||||
expect(result.current.isOpen).toBe(false)
|
||||
expect(result.current.inputError).toBeNull()
|
||||
expect(result.current.snackbarOpen).toBe(false)
|
||||
expect(result.current.snackbarMessage).toBe('')
|
||||
})
|
||||
|
||||
it("should debounce and fetch users when query changes", async () => {
|
||||
const mockUsers = [{ displayName: "John Doe", email: "john@example.com" }];
|
||||
mockSearchUsers.mockResolvedValueOnce(mockUsers);
|
||||
it('should debounce and fetch users when query changes', async () => {
|
||||
const mockUsers = [{ displayName: 'John Doe', email: 'john@example.com' }]
|
||||
mockSearchUsers.mockResolvedValueOnce(mockUsers)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useUserSearch({ objectTypes: ["user"], errorMessage: "Error" })
|
||||
);
|
||||
useUserSearch({ objectTypes: ['user'], errorMessage: 'Error' })
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setQuery("John");
|
||||
});
|
||||
result.current.setQuery('John')
|
||||
})
|
||||
|
||||
expect(result.current.loading).toBe(false); // Before debounce
|
||||
expect(result.current.loading).toBe(false) // Before debounce
|
||||
|
||||
// Wait for the mock promise to resolve within act
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve(); // allow microtasks to flush
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve() // allow microtasks to flush
|
||||
})
|
||||
|
||||
expect(mockSearchUsers).toHaveBeenCalledWith("John", ["user"]);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.options).toEqual(mockUsers);
|
||||
expect(result.current.hasSearched).toBe(true);
|
||||
});
|
||||
expect(mockSearchUsers).toHaveBeenCalledWith('John', ['user'])
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.options).toEqual(mockUsers)
|
||||
expect(result.current.hasSearched).toBe(true)
|
||||
})
|
||||
|
||||
it("should clear options and handle empty query", () => {
|
||||
it('should clear options and handle empty query', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useUserSearch({ objectTypes: ["user"], errorMessage: "Error" })
|
||||
);
|
||||
useUserSearch({ objectTypes: ['user'], errorMessage: 'Error' })
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setQuery(" "); // empty query with spaces
|
||||
});
|
||||
result.current.setQuery(' ') // empty query with spaces
|
||||
})
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
})
|
||||
|
||||
expect(mockSearchUsers).not.toHaveBeenCalled();
|
||||
expect(result.current.options).toEqual([]);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.hasSearched).toBe(false);
|
||||
});
|
||||
expect(mockSearchUsers).not.toHaveBeenCalled()
|
||||
expect(result.current.options).toEqual([])
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.hasSearched).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle search errors and show the custom error message", async () => {
|
||||
mockSearchUsers.mockRejectedValueOnce(new Error("API Error"));
|
||||
it('should handle search errors and show the custom error message', async () => {
|
||||
mockSearchUsers.mockRejectedValueOnce(new Error('API Error'))
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useUserSearch({ objectTypes: ["user"], errorMessage: "Custom Error" })
|
||||
);
|
||||
useUserSearch({ objectTypes: ['user'], errorMessage: 'Custom Error' })
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setQuery("FailedSearch");
|
||||
});
|
||||
result.current.setQuery('FailedSearch')
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
await Promise.resolve(); // allow microtasks to flush
|
||||
});
|
||||
jest.advanceTimersByTime(300)
|
||||
await Promise.resolve() // allow microtasks to flush
|
||||
})
|
||||
|
||||
expect(result.current.hasSearched).toBe(false);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.snackbarOpen).toBe(true);
|
||||
expect(result.current.snackbarMessage).toBe("Custom Error");
|
||||
});
|
||||
});
|
||||
expect(result.current.hasSearched).toBe(false)
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.snackbarOpen).toBe(true)
|
||||
expect(result.current.snackbarMessage).toBe('Custom Error')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,197 +6,197 @@ import {
|
||||
getSecretLink,
|
||||
postCalendar,
|
||||
proppatchCalendar,
|
||||
removeCalendar,
|
||||
} from "@/features/Calendars/CalendarApi";
|
||||
import { clientConfig } from "@/features/User/oidcAuth";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
clientConfig.url = "https://example.com";
|
||||
removeCalendar
|
||||
} from '@/features/Calendars/CalendarApi'
|
||||
import { clientConfig } from '@/features/User/oidcAuth'
|
||||
import { api } from '@/utils/apiUtils'
|
||||
clientConfig.url = 'https://example.com'
|
||||
|
||||
jest.mock("@/utils/apiUtils");
|
||||
jest.mock('@/utils/apiUtils')
|
||||
|
||||
describe("Calendar API", () => {
|
||||
describe('Calendar API', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("fetches calendar list for a user", async () => {
|
||||
const mockUserId = "user123";
|
||||
const mockResponse = [{ id: "calendar1" }, { id: "calendar2" }];
|
||||
it('fetches calendar list for a user', async () => {
|
||||
const mockUserId = 'user123'
|
||||
const mockResponse = [{ id: 'calendar1' }, { id: 'calendar2' }]
|
||||
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockResponse),
|
||||
});
|
||||
;(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockResponse)
|
||||
})
|
||||
|
||||
const calendars = await getCalendars(mockUserId);
|
||||
const calendars = await getCalendars(mockUserId)
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(
|
||||
`dav/calendars/${mockUserId}.json?personal=true&sharedDelegationStatus=accepted&sharedPublicSubscription=true&withRights=true`,
|
||||
{
|
||||
headers: { Accept: "application/calendar+json" },
|
||||
headers: { Accept: 'application/calendar+json' }
|
||||
}
|
||||
);
|
||||
expect(calendars).toEqual(mockResponse);
|
||||
});
|
||||
)
|
||||
expect(calendars).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it("fetches calendar events for a given ID and match window", async () => {
|
||||
const calendarId = "calendar1";
|
||||
const match = { start: "2025-07-01", end: "2025-07-31" };
|
||||
const mockCalendarData = { events: ["event1", "event2"] };
|
||||
it('fetches calendar events for a given ID and match window', async () => {
|
||||
const calendarId = 'calendar1'
|
||||
const match = { start: '2025-07-01', end: '2025-07-31' }
|
||||
const mockCalendarData = { events: ['event1', 'event2'] }
|
||||
|
||||
(api as unknown as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockCalendarData),
|
||||
});
|
||||
;(api as unknown as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockCalendarData)
|
||||
})
|
||||
|
||||
const result = await getCalendar(calendarId, match);
|
||||
const result = await getCalendar(calendarId, match)
|
||||
|
||||
expect(api).toHaveBeenCalledWith(`dav/calendars/${calendarId}.json`, {
|
||||
method: "REPORT",
|
||||
method: 'REPORT',
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
},
|
||||
body: JSON.stringify({ match }),
|
||||
});
|
||||
body: JSON.stringify({ match })
|
||||
})
|
||||
|
||||
expect(result).toEqual(mockCalendarData);
|
||||
});
|
||||
it("postCalendar", async () => {
|
||||
const calId = "calId";
|
||||
const userId = "userId";
|
||||
const color = { light: "calId" };
|
||||
const name = "new cal";
|
||||
const desc = "desc";
|
||||
expect(result).toEqual(mockCalendarData)
|
||||
})
|
||||
it('postCalendar', async () => {
|
||||
const calId = 'calId'
|
||||
const userId = 'userId'
|
||||
const color = { light: 'calId' }
|
||||
const name = 'new cal'
|
||||
const desc = 'desc'
|
||||
|
||||
const result = await postCalendar(userId, calId, color, name, desc);
|
||||
const result = await postCalendar(userId, calId, color, name, desc)
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(`dav/calendars/${userId}.json`, {
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: "calId",
|
||||
"dav:name": "new cal",
|
||||
"apple:color": "calId",
|
||||
"caldav:description": "desc",
|
||||
}),
|
||||
});
|
||||
});
|
||||
it("patch Calendar", async () => {
|
||||
const calId = "calId";
|
||||
const calLink = "/calendars/calId.json";
|
||||
const color = { light: "calIdLight", dark: "calIdDark" };
|
||||
const name = "new cal";
|
||||
const desc = "desc";
|
||||
id: 'calId',
|
||||
'dav:name': 'new cal',
|
||||
'apple:color': 'calId',
|
||||
'caldav:description': 'desc'
|
||||
})
|
||||
})
|
||||
})
|
||||
it('patch Calendar', async () => {
|
||||
const calId = 'calId'
|
||||
const calLink = '/calendars/calId.json'
|
||||
const color = { light: 'calIdLight', dark: 'calIdDark' }
|
||||
const name = 'new cal'
|
||||
const desc = 'desc'
|
||||
|
||||
const result = await proppatchCalendar(calLink, { color, name, desc });
|
||||
const result = await proppatchCalendar(calLink, { color, name, desc })
|
||||
|
||||
expect(api).toHaveBeenCalledWith(`dav${calLink}`, {
|
||||
method: "PROPPATCH",
|
||||
method: 'PROPPATCH',
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"dav:name": "new cal",
|
||||
"caldav:description": "desc",
|
||||
"apple:color": "calIdLight",
|
||||
}),
|
||||
});
|
||||
});
|
||||
'dav:name': 'new cal',
|
||||
'caldav:description': 'desc',
|
||||
'apple:color': 'calIdLight'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("remove Calendar", async () => {
|
||||
const calLink = "/calendars/calId.json";
|
||||
const result = await removeCalendar(calLink);
|
||||
it('remove Calendar', async () => {
|
||||
const calLink = '/calendars/calId.json'
|
||||
const result = await removeCalendar(calLink)
|
||||
|
||||
expect(api).toHaveBeenCalledWith(`dav${calLink}`, {
|
||||
method: "DELETE",
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
},
|
||||
});
|
||||
});
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("get secret link without reset", async () => {
|
||||
const calLink = "/calendars/calId.json";
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue("link"),
|
||||
});
|
||||
it('get secret link without reset', async () => {
|
||||
const calLink = '/calendars/calId.json'
|
||||
;(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue('link')
|
||||
})
|
||||
|
||||
const noreset = await getSecretLink(calLink, false);
|
||||
const noreset = await getSecretLink(calLink, false)
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(
|
||||
`calendar/api${calLink}/secret-link?shouldResetLink=false`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
},
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
it("get secret link with reset", async () => {
|
||||
const calLink = "/calendars/calId.json";
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue("link"),
|
||||
});
|
||||
const reset = await getSecretLink(calLink, true);
|
||||
)
|
||||
})
|
||||
it('get secret link with reset', async () => {
|
||||
const calLink = '/calendars/calId.json'
|
||||
;(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue('link')
|
||||
})
|
||||
const reset = await getSecretLink(calLink, true)
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(
|
||||
`calendar/api${calLink}/secret-link?shouldResetLink=true`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
},
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("get export data ", async () => {
|
||||
const calLink = "/calendars/calId.json";
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
text: jest.fn().mockResolvedValue("data"),
|
||||
});
|
||||
const data = await exportCalendar(calLink);
|
||||
it('get export data ', async () => {
|
||||
const calLink = '/calendars/calId.json'
|
||||
;(api.get as jest.Mock).mockReturnValue({
|
||||
text: jest.fn().mockResolvedValue('data')
|
||||
})
|
||||
const data = await exportCalendar(calLink)
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(`dav${calLink}?export`, {
|
||||
headers: {
|
||||
Accept: "application/calendar",
|
||||
},
|
||||
});
|
||||
});
|
||||
Accept: 'application/calendar'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("When adding a sharedCal with #default #default is preserved", async () => {
|
||||
const mockApiPost = jest.spyOn(api, "post");
|
||||
it('When adding a sharedCal with #default #default is preserved', async () => {
|
||||
const mockApiPost = jest.spyOn(api, 'post')
|
||||
|
||||
const calData = {
|
||||
cal: {
|
||||
id: "cal123",
|
||||
"dav:name": "#default",
|
||||
"apple:color": "#FF5733",
|
||||
"caldav:description": "Default calendar",
|
||||
id: 'cal123',
|
||||
'dav:name': '#default',
|
||||
'apple:color': '#FF5733',
|
||||
'caldav:description': 'Default calendar',
|
||||
acl: [],
|
||||
invite: [],
|
||||
_links: {
|
||||
self: {
|
||||
href: "/calendars/owner123/cal123.json",
|
||||
},
|
||||
},
|
||||
href: '/calendars/owner123/cal123.json'
|
||||
}
|
||||
}
|
||||
},
|
||||
owner: {
|
||||
displayName: "John Doe",
|
||||
email: "john.doe@example.com",
|
||||
openpaasId: "owner123",
|
||||
displayName: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
openpaasId: 'owner123'
|
||||
},
|
||||
color: "#FF5733",
|
||||
};
|
||||
color: '#FF5733'
|
||||
}
|
||||
|
||||
await addSharedCalendar("currentUserId", "newCalId123", calData);
|
||||
await addSharedCalendar('currentUserId', 'newCalId123', calData)
|
||||
|
||||
expect(mockApiPost).toHaveBeenCalledWith(
|
||||
"dav/calendars/currentUserId.json",
|
||||
'dav/calendars/currentUserId.json',
|
||||
expect.objectContaining({
|
||||
body: expect.stringContaining('"dav:name":"#default"'),
|
||||
body: expect.stringContaining('"dav:name":"#default"')
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
const callBody = JSON.parse(String(mockApiPost.mock.calls[0][1]?.body));
|
||||
expect(callBody["dav:name"]).toBe("#default");
|
||||
});
|
||||
});
|
||||
const callBody = JSON.parse(String(mockApiPost.mock.calls[0][1]?.body))
|
||||
expect(callBody['dav:name']).toBe('#default')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
import { AccessTab } from "@/components/Calendar/AccessTab";
|
||||
import { AccessTab } from '@/components/Calendar/AccessTab'
|
||||
import {
|
||||
CalendarAccessRights,
|
||||
UserWithAccess,
|
||||
} from "@/components/Calendar/CalendarAccessRights";
|
||||
import CalendarPopover from "@/components/Calendar/CalendarModal";
|
||||
import { updateDelegationCalendar } from "@/features/Calendars/api/updateDelegationCalendar";
|
||||
import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import * as delegationThunks from "@/features/Calendars/services/updateDelegationCalendarAsync";
|
||||
import { getUserDetails } from "@/features/User/userAPI";
|
||||
import { accessRightToDavProp } from "@/utils/accessRightToDavProp";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
UserWithAccess
|
||||
} from '@/components/Calendar/CalendarAccessRights'
|
||||
import CalendarPopover from '@/components/Calendar/CalendarModal'
|
||||
import { updateDelegationCalendar } from '@/features/Calendars/api/updateDelegationCalendar'
|
||||
import { AccessRight, Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import * as delegationThunks from '@/features/Calendars/services/updateDelegationCalendarAsync'
|
||||
import { getUserDetails } from '@/features/User/userAPI'
|
||||
import { accessRightToDavProp } from '@/utils/accessRightToDavProp'
|
||||
import { api } from '@/utils/apiUtils'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/utils/apiUtils", () => ({
|
||||
api: { post: jest.fn() },
|
||||
}));
|
||||
jest.mock('@/utils/apiUtils', () => ({
|
||||
api: { post: jest.fn() }
|
||||
}))
|
||||
|
||||
jest.mock("@/features/User/userAPI", () => ({
|
||||
getUserDetails: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/features/User/userAPI', () => ({
|
||||
getUserDetails: jest.fn()
|
||||
}))
|
||||
|
||||
jest.mock("@/features/Calendars/CalendarApi", () => ({
|
||||
getSecretLink: jest.fn().mockReturnValue(""),
|
||||
exportCalendar: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/features/Calendars/CalendarApi', () => ({
|
||||
getSecretLink: jest.fn().mockReturnValue(''),
|
||||
exportCalendar: jest.fn()
|
||||
}))
|
||||
|
||||
const mockThunkWithUnwrap = (resolvedValue: unknown = {}) =>
|
||||
jest.fn().mockImplementation(() => {
|
||||
const result = Object.assign(Promise.resolve(resolvedValue), {
|
||||
unwrap: () => Promise.resolve(resolvedValue),
|
||||
});
|
||||
return jest.fn().mockReturnValue(result);
|
||||
});
|
||||
unwrap: () => Promise.resolve(resolvedValue)
|
||||
})
|
||||
return jest.fn().mockReturnValue(result)
|
||||
})
|
||||
|
||||
describe("accessRightToDavProp", () => {
|
||||
it("maps 5 (ADMIN) → dav:administration", () => {
|
||||
expect(accessRightToDavProp(5)).toBe("dav:administration");
|
||||
});
|
||||
describe('accessRightToDavProp', () => {
|
||||
it('maps 5 (ADMIN) → dav:administration', () => {
|
||||
expect(accessRightToDavProp(5)).toBe('dav:administration')
|
||||
})
|
||||
|
||||
it("maps 3 (EDITOR) → dav:read-write", () => {
|
||||
expect(accessRightToDavProp(3)).toBe("dav:read-write");
|
||||
});
|
||||
it('maps 3 (EDITOR) → dav:read-write', () => {
|
||||
expect(accessRightToDavProp(3)).toBe('dav:read-write')
|
||||
})
|
||||
|
||||
it("maps 2 (VIEW) → dav:read", () => {
|
||||
expect(accessRightToDavProp(2)).toBe("dav:read");
|
||||
});
|
||||
it('maps 2 (VIEW) → dav:read', () => {
|
||||
expect(accessRightToDavProp(2)).toBe('dav:read')
|
||||
})
|
||||
|
||||
it("defaults to dav:read for unknown values (covered by default case)", () => {
|
||||
expect(accessRightToDavProp(999 as AccessRight)).toBe("dav:read");
|
||||
});
|
||||
});
|
||||
it('defaults to dav:read for unknown values (covered by default case)', () => {
|
||||
expect(accessRightToDavProp(999 as AccessRight)).toBe('dav:read')
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateDelegationCalendar", () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
describe('updateDelegationCalendar', () => {
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
|
||||
it("posts to the correct DAV endpoint with the share body", async () => {
|
||||
(api.post as jest.Mock).mockResolvedValue({ ok: true });
|
||||
it('posts to the correct DAV endpoint with the share body', async () => {
|
||||
;(api.post as jest.Mock).mockResolvedValue({ ok: true })
|
||||
|
||||
const share = {
|
||||
set: [{ "dav:href": "mailto:alice@example.com", "dav:read": true }],
|
||||
remove: [],
|
||||
};
|
||||
set: [{ 'dav:href': 'mailto:alice@example.com', 'dav:read': true }],
|
||||
remove: []
|
||||
}
|
||||
|
||||
await updateDelegationCalendar("/calendars/user/cal1.json", share);
|
||||
await updateDelegationCalendar('/calendars/user/cal1.json', share)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
"dav/calendars/user/cal1.json",
|
||||
'dav/calendars/user/cal1.json',
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({ share }),
|
||||
body: JSON.stringify({ share })
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const baseCalendar: Calendar = {
|
||||
id: "user1/cal1",
|
||||
name: "My Calendar",
|
||||
description: "",
|
||||
color: { color: "#0062FF", dark: "#FFF" },
|
||||
link: "/calendars/user1/cal1.json",
|
||||
visibility: "public",
|
||||
id: 'user1/cal1',
|
||||
name: 'My Calendar',
|
||||
description: '',
|
||||
color: { color: '#0062FF', dark: '#FFF' },
|
||||
link: '/calendars/user1/cal1.json',
|
||||
visibility: 'public',
|
||||
events: {},
|
||||
invite: [],
|
||||
owner: { emails: ["user1@example.com"] },
|
||||
owner: { emails: ['user1@example.com'] },
|
||||
access: { write: true } as any,
|
||||
delegated: true,
|
||||
};
|
||||
delegated: true
|
||||
}
|
||||
|
||||
describe("CalendarAccessRights", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const mockOnInvitesLoaded = jest.fn();
|
||||
describe('CalendarAccessRights', () => {
|
||||
const mockOnChange = jest.fn()
|
||||
const mockOnInvitesLoaded = jest.fn()
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
|
||||
it("renders the grant access rights section", () => {
|
||||
it('renders the grant access rights section', () => {
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
calendar={baseCalendar}
|
||||
@@ -106,22 +106,22 @@ describe("CalendarAccessRights", () => {
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText("calendarPopover.access.grantAccessRights")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getByText('calendarPopover.access.grantAccessRights')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows a list of users already passed in via value prop", () => {
|
||||
it('shows a list of users already passed in via value prop', () => {
|
||||
const users: UserWithAccess[] = [
|
||||
{
|
||||
openpaasId: "user1",
|
||||
displayName: "Alice",
|
||||
email: "alice@example.com",
|
||||
accessRight: 2,
|
||||
},
|
||||
];
|
||||
openpaasId: 'user1',
|
||||
displayName: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
accessRight: 2
|
||||
}
|
||||
]
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
@@ -130,21 +130,21 @@ describe("CalendarAccessRights", () => {
|
||||
onChange={mockOnChange}
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByText("Alice")).toBeInTheDocument();
|
||||
expect(screen.getByText("alice@example.com")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument()
|
||||
expect(screen.getByText('alice@example.com')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onChange with user removed when remove button is clicked", () => {
|
||||
it('calls onChange with user removed when remove button is clicked', () => {
|
||||
const users: UserWithAccess[] = [
|
||||
{
|
||||
openpaasId: "user1",
|
||||
displayName: "Alice",
|
||||
email: "alice@example.com",
|
||||
accessRight: 5,
|
||||
},
|
||||
];
|
||||
openpaasId: 'user1',
|
||||
displayName: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
accessRight: 5
|
||||
}
|
||||
]
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
@@ -154,31 +154,31 @@ describe("CalendarAccessRights", () => {
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByLabelText(/remove/i));
|
||||
expect(mockOnChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText(/remove/i))
|
||||
expect(mockOnChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it("loads invited users from calendar.invite on mount", async () => {
|
||||
it('loads invited users from calendar.invite on mount', async () => {
|
||||
const calendarWithInvite: Calendar = {
|
||||
...baseCalendar,
|
||||
invite: [
|
||||
{
|
||||
href: "mailto:bob@example.com",
|
||||
principal: "/principals/users/bob123",
|
||||
href: 'mailto:bob@example.com',
|
||||
principal: '/principals/users/bob123',
|
||||
access: 3,
|
||||
inviteStatus: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
inviteStatus: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
(getUserDetails as jest.Mock).mockResolvedValue({
|
||||
preferredEmail: "bob@example.com",
|
||||
firstname: "Bob",
|
||||
lastname: "Smith",
|
||||
emails: ["bob@example.com"],
|
||||
});
|
||||
;(getUserDetails as jest.Mock).mockResolvedValue({
|
||||
preferredEmail: 'bob@example.com',
|
||||
firstname: 'Bob',
|
||||
lastname: 'Smith',
|
||||
emails: ['bob@example.com']
|
||||
})
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
@@ -188,35 +188,35 @@ describe("CalendarAccessRights", () => {
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith("bob123"));
|
||||
await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith('bob123'))
|
||||
await waitFor(() =>
|
||||
expect(mockOnInvitesLoaded).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: "bob@example.com",
|
||||
accessRight: 3,
|
||||
}),
|
||||
email: 'bob@example.com',
|
||||
accessRight: 3
|
||||
})
|
||||
])
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("skips invite entries where getUserDetails throws", async () => {
|
||||
it('skips invite entries where getUserDetails throws', async () => {
|
||||
const calendarWithInvite: Calendar = {
|
||||
...baseCalendar,
|
||||
invite: [
|
||||
{
|
||||
href: "mailto:ghost@example.com",
|
||||
principal: "/principals/users/ghost",
|
||||
href: 'mailto:ghost@example.com',
|
||||
principal: '/principals/users/ghost',
|
||||
access: 2,
|
||||
inviteStatus: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
inviteStatus: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
(getUserDetails as jest.Mock).mockRejectedValue(new Error("Not found"));
|
||||
;(getUserDetails as jest.Mock).mockRejectedValue(new Error('Not found'))
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
@@ -226,28 +226,28 @@ describe("CalendarAccessRights", () => {
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([]));
|
||||
});
|
||||
await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([]))
|
||||
})
|
||||
|
||||
it("shows a loading spinner while invite users are being fetched", async () => {
|
||||
it('shows a loading spinner while invite users are being fetched', async () => {
|
||||
const calendarWithInvite: Calendar = {
|
||||
...baseCalendar,
|
||||
invite: [
|
||||
{
|
||||
href: "mailto:carol@example.com",
|
||||
principal: "/principals/users/carol",
|
||||
href: 'mailto:carol@example.com',
|
||||
principal: '/principals/users/carol',
|
||||
access: 2,
|
||||
inviteStatus: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
inviteStatus: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Never resolves during the assertion window
|
||||
(getUserDetails as jest.Mock).mockImplementation(
|
||||
;(getUserDetails as jest.Mock).mockImplementation(
|
||||
() => new Promise(() => {})
|
||||
);
|
||||
)
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
@@ -257,12 +257,12 @@ describe("CalendarAccessRights", () => {
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument();
|
||||
});
|
||||
expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("displays the calendar owner information", () => {
|
||||
it('displays the calendar owner information', () => {
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
calendar={baseCalendar}
|
||||
@@ -271,46 +271,44 @@ describe("CalendarAccessRights", () => {
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getAllByText("user1@example.com").length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getByText("calendarPopover.access.owner")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getAllByText('user1@example.com').length).toBeGreaterThan(0)
|
||||
expect(screen.getByText('calendarPopover.access.owner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("fetches and displays resource administrators for resource calendars", async () => {
|
||||
it('fetches and displays resource administrators for resource calendars', async () => {
|
||||
const resourceCalendar: any = {
|
||||
...baseCalendar,
|
||||
owner: {
|
||||
_id: "resource1",
|
||||
_id: 'resource1',
|
||||
resource: true,
|
||||
emails: ["resource1@example.com"],
|
||||
emails: ['resource1@example.com'],
|
||||
administrators: [
|
||||
{ id: "admin1" },
|
||||
{ id: "admin2" },
|
||||
{ id: "resource1" }, // Owner shouldn't be loaded as an admin
|
||||
],
|
||||
},
|
||||
};
|
||||
{ id: 'admin1' },
|
||||
{ id: 'admin2' },
|
||||
{ id: 'resource1' } // Owner shouldn't be loaded as an admin
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
(getUserDetails as jest.Mock).mockImplementation((id: string) => {
|
||||
if (id === "admin1") {
|
||||
;(getUserDetails as jest.Mock).mockImplementation((id: string) => {
|
||||
if (id === 'admin1') {
|
||||
return Promise.resolve({
|
||||
preferredEmail: "admin1@example.com",
|
||||
firstname: "Admin",
|
||||
lastname: "One",
|
||||
});
|
||||
preferredEmail: 'admin1@example.com',
|
||||
firstname: 'Admin',
|
||||
lastname: 'One'
|
||||
})
|
||||
}
|
||||
if (id === "admin2") {
|
||||
if (id === 'admin2') {
|
||||
return Promise.resolve({
|
||||
preferredEmail: "admin2@example.com",
|
||||
firstname: "Admin",
|
||||
lastname: "Two",
|
||||
});
|
||||
preferredEmail: 'admin2@example.com',
|
||||
firstname: 'Admin',
|
||||
lastname: 'Two'
|
||||
})
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarAccessRights
|
||||
@@ -320,37 +318,37 @@ describe("CalendarAccessRights", () => {
|
||||
onInvitesLoaded={mockOnInvitesLoaded}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getUserDetails).toHaveBeenCalledWith("admin1");
|
||||
expect(getUserDetails).toHaveBeenCalledWith("admin2");
|
||||
});
|
||||
expect(getUserDetails).toHaveBeenCalledWith('admin1')
|
||||
expect(getUserDetails).toHaveBeenCalledWith('admin2')
|
||||
})
|
||||
|
||||
expect(getUserDetails).not.toHaveBeenCalledWith("resource1");
|
||||
expect(getUserDetails).not.toHaveBeenCalledWith('resource1')
|
||||
|
||||
expect(await screen.findByText("Admin One")).toBeInTheDocument();
|
||||
expect(screen.getByText("admin1@example.com")).toBeInTheDocument();
|
||||
expect(screen.getByText("Admin Two")).toBeInTheDocument();
|
||||
expect(screen.getByText("admin2@example.com")).toBeInTheDocument();
|
||||
expect(await screen.findByText('Admin One')).toBeInTheDocument()
|
||||
expect(screen.getByText('admin1@example.com')).toBeInTheDocument()
|
||||
expect(screen.getByText('Admin Two')).toBeInTheDocument()
|
||||
expect(screen.getByText('admin2@example.com')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getAllByText("calendarPopover.access.administrator")
|
||||
).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
screen.getAllByText('calendarPopover.access.administrator')
|
||||
).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
const userState = {
|
||||
user: {
|
||||
userData: { openpaasId: "user1", email: "user1@example.com" },
|
||||
},
|
||||
};
|
||||
userData: { openpaasId: 'user1', email: 'user1@example.com' }
|
||||
}
|
||||
}
|
||||
|
||||
describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
|
||||
const noop = jest.fn();
|
||||
describe('AccessTab – conditional rendering of CalendarAccessRights', () => {
|
||||
const noop = jest.fn()
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
afterEach(() => jest.clearAllMocks())
|
||||
|
||||
it("shows CalendarAccessRights when the current user owns the calendar", () => {
|
||||
it('shows CalendarAccessRights when the current user owns the calendar', () => {
|
||||
renderWithProviders(
|
||||
<AccessTab
|
||||
calendar={baseCalendar}
|
||||
@@ -360,21 +358,21 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
|
||||
/>,
|
||||
{
|
||||
...userState,
|
||||
calendars: { list: { "user1/cal1": baseCalendar } },
|
||||
calendars: { list: { 'user1/cal1': baseCalendar } }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText("calendarPopover.access.grantAccessRights")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getByText('calendarPopover.access.grantAccessRights')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("hides CalendarAccessRights textfield for a non-owner without admin delegation", () => {
|
||||
it('hides CalendarAccessRights textfield for a non-owner without admin delegation', () => {
|
||||
const foreignCalendar: Calendar = {
|
||||
...baseCalendar,
|
||||
id: "otherUser/cal1",
|
||||
invite: [],
|
||||
};
|
||||
id: 'otherUser/cal1',
|
||||
invite: []
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<AccessTab
|
||||
@@ -385,29 +383,29 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
|
||||
/>,
|
||||
{
|
||||
...userState,
|
||||
calendars: { list: { "otherUser/cal1": foreignCalendar } },
|
||||
calendars: { list: { 'otherUser/cal1': foreignCalendar } }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.queryByText("peopleSearch.label")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('peopleSearch.label')).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText("calendarPopover.access.accessRights")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getByText('calendarPopover.access.accessRights')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows CalendarAccessRights when the user has access=5 (admin) delegation", () => {
|
||||
it('shows CalendarAccessRights when the user has access=5 (admin) delegation', () => {
|
||||
const delegatedCalendar: Calendar = {
|
||||
...baseCalendar,
|
||||
id: "otherUser/cal1",
|
||||
id: 'otherUser/cal1',
|
||||
invite: [
|
||||
{
|
||||
href: "mailto:user1@example.com",
|
||||
principal: "/principals/users/user1",
|
||||
href: 'mailto:user1@example.com',
|
||||
principal: '/principals/users/user1',
|
||||
access: 5,
|
||||
inviteStatus: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
inviteStatus: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<AccessTab
|
||||
@@ -418,78 +416,78 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
|
||||
/>,
|
||||
{
|
||||
...userState,
|
||||
calendars: { list: { "otherUser/cal1": delegatedCalendar } },
|
||||
calendars: { list: { 'otherUser/cal1': delegatedCalendar } }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText("calendarPopover.access.grantAccessRights")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
screen.getByText('calendarPopover.access.grantAccessRights')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
const existingCalendar: Calendar = {
|
||||
id: "user1/cal1",
|
||||
name: "Existing Cal",
|
||||
description: "Desc",
|
||||
color: { color: "#0062FF", dark: "#FFF" },
|
||||
link: "/calendars/user/cal1",
|
||||
visibility: "public",
|
||||
id: 'user1/cal1',
|
||||
name: 'Existing Cal',
|
||||
description: 'Desc',
|
||||
color: { color: '#0062FF', dark: '#FFF' },
|
||||
link: '/calendars/user/cal1',
|
||||
visibility: 'public',
|
||||
events: {},
|
||||
invite: [],
|
||||
owner: { emails: ["user1@example.com"] },
|
||||
};
|
||||
owner: { emails: ['user1@example.com'] }
|
||||
}
|
||||
|
||||
describe("CalendarModal – updateDelegationCalendarAsync integration", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('CalendarModal – updateDelegationCalendarAsync integration', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
jest
|
||||
.spyOn(eventThunks, "patchCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'patchCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
jest
|
||||
.spyOn(eventThunks, "patchACLCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'patchACLCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
jest
|
||||
.spyOn(delegationThunks, "updateDelegationCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
});
|
||||
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
})
|
||||
|
||||
it("does NOT call updateDelegationCalendarAsync when no users are added or removed", async () => {
|
||||
it('does NOT call updateDelegationCalendarAsync when no users are added or removed', async () => {
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ ...userState, calendars: { list: { "user1/cal1": existingCalendar } } }
|
||||
);
|
||||
{ ...userState, calendars: { list: { 'user1/cal1': existingCalendar } } }
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled())
|
||||
|
||||
expect(
|
||||
delegationThunks.updateDelegationCalendarAsync
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("CalendarModal – cancel button", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('CalendarModal – cancel button', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
jest
|
||||
.spyOn(delegationThunks, "updateDelegationCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
jest
|
||||
.spyOn(eventThunks, "patchCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
});
|
||||
.spyOn(eventThunks, 'patchCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
})
|
||||
|
||||
it("calls onClose without saving when Cancel is clicked", async () => {
|
||||
it('calls onClose without saving when Cancel is clicked', async () => {
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
open={true}
|
||||
@@ -497,14 +495,14 @@ describe("CalendarModal – cancel button", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ ...userState }
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }))
|
||||
|
||||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled())
|
||||
expect(
|
||||
delegationThunks.updateDelegationCalendarAsync
|
||||
).not.toHaveBeenCalled();
|
||||
expect(eventThunks.patchCalendarAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
).not.toHaveBeenCalled()
|
||||
expect(eventThunks.patchCalendarAsync).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
import CalendarPopover from "@/components/Calendar/CalendarModal";
|
||||
import { getSecretLink } from "@/features/Calendars/CalendarApi";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import * as delegationThunks from "@/features/Calendars/services/updateDelegationCalendarAsync";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import CalendarPopover from '@/components/Calendar/CalendarModal'
|
||||
import { getSecretLink } from '@/features/Calendars/CalendarApi'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import * as eventThunks from '@/features/Calendars/services'
|
||||
import * as delegationThunks from '@/features/Calendars/services/updateDelegationCalendarAsync'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/features/Calendars/CalendarApi", () => ({
|
||||
getSecretLink: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/features/Calendars/CalendarApi', () => ({
|
||||
getSecretLink: jest.fn()
|
||||
}))
|
||||
|
||||
const mockThunkWithUnwrap = (resolvedValue: unknown = {}) =>
|
||||
jest.fn().mockImplementation(() => {
|
||||
const dispatchResult = Object.assign(Promise.resolve(resolvedValue), {
|
||||
unwrap: () => Promise.resolve(resolvedValue),
|
||||
});
|
||||
return jest.fn().mockReturnValue(dispatchResult);
|
||||
});
|
||||
unwrap: () => Promise.resolve(resolvedValue)
|
||||
})
|
||||
return jest.fn().mockReturnValue(dispatchResult)
|
||||
})
|
||||
|
||||
describe("CalendarPopover", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('CalendarPopover', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
jest
|
||||
.spyOn(delegationThunks, "updateDelegationCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
});
|
||||
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
})
|
||||
|
||||
const renderPopover = (open = true) => {
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
},
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
}
|
||||
},
|
||||
calendars: { list: {}, pending: true },
|
||||
};
|
||||
calendars: { list: {}, pending: true }
|
||||
}
|
||||
renderWithProviders(
|
||||
<CalendarPopover open={open} onClose={mockOnClose} />,
|
||||
preloadedState
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
it("renders popover and inputs", () => {
|
||||
renderPopover();
|
||||
it('renders popover and inputs', () => {
|
||||
renderPopover()
|
||||
|
||||
expect(screen.getByLabelText(/Name/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("event.form.addDescription")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByLabelText(/Name/i)).toBeInTheDocument()
|
||||
expect(screen.getByText('event.form.addDescription')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("updates name and description fields", () => {
|
||||
renderPopover();
|
||||
it('updates name and description fields', () => {
|
||||
renderPopover()
|
||||
|
||||
const nameInput = screen.getByLabelText(/Name/i);
|
||||
fireEvent.change(nameInput, { target: { value: "My Calendar" } });
|
||||
expect(nameInput).toHaveValue("My Calendar");
|
||||
const nameInput = screen.getByLabelText(/Name/i)
|
||||
fireEvent.change(nameInput, { target: { value: 'My Calendar' } })
|
||||
expect(nameInput).toHaveValue('My Calendar')
|
||||
|
||||
fireEvent.click(screen.getByText("event.form.addDescription"));
|
||||
const descInput = screen.getByLabelText(/Description/i);
|
||||
fireEvent.change(descInput, { target: { value: "Test description" } });
|
||||
expect(descInput).toHaveValue("Test description");
|
||||
});
|
||||
fireEvent.click(screen.getByText('event.form.addDescription'))
|
||||
const descInput = screen.getByLabelText(/Description/i)
|
||||
fireEvent.change(descInput, { target: { value: 'Test description' } })
|
||||
expect(descInput).toHaveValue('Test description')
|
||||
})
|
||||
|
||||
it("dispatches createCalendar and calls onClose when Save clicked", async () => {
|
||||
it('dispatches createCalendar and calls onClose when Save clicked', async () => {
|
||||
jest
|
||||
.spyOn(eventThunks, "createCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'createCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
|
||||
renderPopover();
|
||||
renderPopover()
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/Name/i), {
|
||||
target: { value: "Test Calendar" },
|
||||
});
|
||||
fireEvent.click(screen.getByText("event.form.addDescription"));
|
||||
target: { value: 'Test Calendar' }
|
||||
})
|
||||
fireEvent.click(screen.getByText('event.form.addDescription'))
|
||||
fireEvent.change(screen.getByLabelText(/Description/i), {
|
||||
target: { value: "Test Description" },
|
||||
});
|
||||
target: { value: 'Test Description' }
|
||||
})
|
||||
|
||||
const colorButtons = screen.getAllByRole("button", {
|
||||
name: /select color/i,
|
||||
});
|
||||
fireEvent.click(colorButtons[0]);
|
||||
const colorButtons = screen.getAllByRole('button', {
|
||||
name: /select color/i
|
||||
})
|
||||
fireEvent.click(colorButtons[0])
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Create/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Create/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(eventThunks.createCalendarAsync).toHaveBeenCalled()
|
||||
);
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick")
|
||||
);
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
|
||||
)
|
||||
})
|
||||
|
||||
it("calls onClose when Cancel clicked", () => {
|
||||
renderPopover();
|
||||
it('calls onClose when Cancel clicked', () => {
|
||||
renderPopover()
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Cancel/i }))
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick')
|
||||
})
|
||||
})
|
||||
|
||||
describe("CalendarPopover (editing mode)", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('CalendarPopover (editing mode)', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
const baseUser = {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
},
|
||||
};
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
}
|
||||
}
|
||||
|
||||
const existingCalendar: Calendar = {
|
||||
id: "user1/cal1",
|
||||
link: "/calendars/user/cal1",
|
||||
name: "Work Calendar",
|
||||
description: "Team meetings",
|
||||
color: { light: "#33B679" },
|
||||
owner: { firstname: "alice", emails: ["alice@example.com"] },
|
||||
visibility: "public",
|
||||
events: {},
|
||||
};
|
||||
id: 'user1/cal1',
|
||||
link: '/calendars/user/cal1',
|
||||
name: 'Work Calendar',
|
||||
description: 'Team meetings',
|
||||
color: { light: '#33B679' },
|
||||
owner: { firstname: 'alice', emails: ['alice@example.com'] },
|
||||
visibility: 'public',
|
||||
events: {}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
jest
|
||||
.spyOn(delegationThunks, "updateDelegationCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
});
|
||||
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
})
|
||||
|
||||
it("prefills fields when calendar prop is given", () => {
|
||||
it('prefills fields when calendar prop is given', () => {
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
open={true}
|
||||
@@ -143,34 +143,34 @@ describe("CalendarPopover (editing mode)", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ user: baseUser }
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue("Work Calendar");
|
||||
expect(screen.getByLabelText(/Description/i)).toHaveValue("Team meetings");
|
||||
});
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('Work Calendar')
|
||||
expect(screen.getByLabelText(/Description/i)).toHaveValue('Team meetings')
|
||||
})
|
||||
|
||||
test("Save button is disabled when name is empty or whitespace only", () => {
|
||||
test('Save button is disabled when name is empty or whitespace only', () => {
|
||||
renderWithProviders(<CalendarPopover open={true} onClose={jest.fn()} />, {
|
||||
user: baseUser,
|
||||
});
|
||||
user: baseUser
|
||||
})
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /create/i });
|
||||
expect(saveButton).toBeDisabled();
|
||||
const saveButton = screen.getByRole('button', { name: /create/i })
|
||||
expect(saveButton).toBeDisabled()
|
||||
// only spaces
|
||||
const nameInput = screen.getByLabelText(/name/i);
|
||||
fireEvent.change(nameInput, { target: { value: " " } });
|
||||
const nameInput = screen.getByLabelText(/name/i)
|
||||
fireEvent.change(nameInput, { target: { value: ' ' } })
|
||||
|
||||
expect(saveButton).toBeDisabled();
|
||||
expect(saveButton).toBeDisabled()
|
||||
|
||||
// valid name
|
||||
fireEvent.change(nameInput, { target: { value: "Work Calendar" } });
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
fireEvent.change(nameInput, { target: { value: 'Work Calendar' } })
|
||||
expect(saveButton).toBeEnabled()
|
||||
})
|
||||
|
||||
it("allows modifying and saving existing calendar", async () => {
|
||||
it('allows modifying and saving existing calendar', async () => {
|
||||
jest
|
||||
.spyOn(eventThunks, "patchCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'patchCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
@@ -179,87 +179,87 @@ describe("CalendarPopover (editing mode)", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ user: baseUser }
|
||||
);
|
||||
)
|
||||
|
||||
// Change name
|
||||
fireEvent.change(screen.getByLabelText(/Name/i), {
|
||||
target: { value: "Updated Calendar" },
|
||||
});
|
||||
target: { value: 'Updated Calendar' }
|
||||
})
|
||||
|
||||
// Save
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(eventThunks.patchCalendarAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
calId: "user1/cal1",
|
||||
calLink: "/calendars/user/cal1",
|
||||
calId: 'user1/cal1',
|
||||
calLink: '/calendars/user/cal1',
|
||||
patch: {
|
||||
color: { light: "#33B679" },
|
||||
desc: "Team meetings",
|
||||
name: "Updated Calendar",
|
||||
},
|
||||
color: { light: '#33B679' },
|
||||
desc: 'Team meetings',
|
||||
name: 'Updated Calendar'
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
)
|
||||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled())
|
||||
})
|
||||
})
|
||||
|
||||
describe("CalendarPopover - Tabs Scenarios", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('CalendarPopover - Tabs Scenarios', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
const baseUser = {
|
||||
userData: {
|
||||
openpaasId: "user1",
|
||||
},
|
||||
};
|
||||
openpaasId: 'user1'
|
||||
}
|
||||
}
|
||||
|
||||
const writeText = jest.fn();
|
||||
const writeText = jest.fn()
|
||||
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText,
|
||||
},
|
||||
});
|
||||
writeText
|
||||
}
|
||||
})
|
||||
|
||||
const existingCalendar: Calendar = {
|
||||
id: "user1/cal1",
|
||||
link: "/calendars/user1/cal1.json",
|
||||
name: "Work Calendar",
|
||||
description: "Team meetings",
|
||||
color: { light: "#33B679" },
|
||||
owner: { firstname: "alice", emails: ["alice@example.com"] },
|
||||
visibility: "public",
|
||||
events: {},
|
||||
};
|
||||
id: 'user1/cal1',
|
||||
link: '/calendars/user1/cal1.json',
|
||||
name: 'Work Calendar',
|
||||
description: 'Team meetings',
|
||||
color: { light: '#33B679' },
|
||||
owner: { firstname: 'alice', emails: ['alice@example.com'] },
|
||||
visibility: 'public',
|
||||
events: {}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
jest
|
||||
.spyOn(delegationThunks, "updateDelegationCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
});
|
||||
.spyOn(delegationThunks, 'updateDelegationCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
})
|
||||
|
||||
it("resets state after closing and reopening", () => {
|
||||
it('resets state after closing and reopening', () => {
|
||||
const { rerender } = renderWithProviders(
|
||||
<CalendarPopover open={true} onClose={mockOnClose} />,
|
||||
{ user: baseUser }
|
||||
);
|
||||
)
|
||||
|
||||
// Enter some data
|
||||
fireEvent.change(screen.getByLabelText(/Name/i), {
|
||||
target: { value: "Temp Calendar" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
|
||||
target: { value: 'Temp Calendar' }
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: /Cancel/i }))
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
|
||||
// Reopen: state should be reset
|
||||
rerender(<CalendarPopover open={true} onClose={mockOnClose} />);
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue("");
|
||||
});
|
||||
rerender(<CalendarPopover open={true} onClose={mockOnClose} />)
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('')
|
||||
})
|
||||
|
||||
it("shows Access tab only when editing an existing calendar", () => {
|
||||
it('shows Access tab only when editing an existing calendar', () => {
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
open={true}
|
||||
@@ -267,29 +267,29 @@ describe("CalendarPopover - Tabs Scenarios", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ user: baseUser }
|
||||
);
|
||||
)
|
||||
|
||||
expect(screen.getByRole("tab", { name: /Access/i })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('tab', { name: /Access/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not show Access tab when creating new calendar", () => {
|
||||
it('does not show Access tab when creating new calendar', () => {
|
||||
renderWithProviders(<CalendarPopover open={true} onClose={mockOnClose} />, {
|
||||
user: baseUser,
|
||||
});
|
||||
user: baseUser
|
||||
})
|
||||
|
||||
expect(
|
||||
screen.queryByRole("tab", { name: /Access/i })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
screen.queryByRole('tab', { name: /Access/i })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("patches ACL when visibility changes", async () => {
|
||||
it('patches ACL when visibility changes', async () => {
|
||||
jest
|
||||
.spyOn(eventThunks, "patchCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'patchCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
|
||||
jest
|
||||
.spyOn(eventThunks, "patchACLCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'patchACLCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
@@ -298,42 +298,42 @@ describe("CalendarPopover - Tabs Scenarios", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ user: baseUser }
|
||||
);
|
||||
)
|
||||
|
||||
// By default: "All" (public) is selected
|
||||
const publicButton = screen.getByRole("button", { name: /All/i });
|
||||
const privateButton = screen.getByRole("button", { name: /You/i });
|
||||
const publicButton = screen.getByRole('button', { name: /All/i })
|
||||
const privateButton = screen.getByRole('button', { name: /You/i })
|
||||
|
||||
expect(publicButton).toHaveAttribute("aria-pressed", "true");
|
||||
expect(privateButton).toHaveAttribute("aria-pressed", "false");
|
||||
expect(publicButton).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(privateButton).toHaveAttribute('aria-pressed', 'false')
|
||||
|
||||
// Change to private
|
||||
fireEvent.click(privateButton);
|
||||
fireEvent.click(privateButton)
|
||||
|
||||
expect(privateButton).toHaveAttribute("aria-pressed", "true");
|
||||
expect(publicButton).toHaveAttribute("aria-pressed", "false");
|
||||
expect(privateButton).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(publicButton).toHaveAttribute('aria-pressed', 'false')
|
||||
|
||||
// Save
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.save' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(eventThunks.patchACLCalendarAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
calId: "user1/cal1",
|
||||
request: "",
|
||||
calId: 'user1/cal1',
|
||||
request: ''
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("copies CalDAV link from Access tab", async () => {
|
||||
window.DAV_BASE_URL = "https://cal.example.org";
|
||||
it('copies CalDAV link from Access tab', async () => {
|
||||
window.DAV_BASE_URL = 'https://cal.example.org'
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: jest.fn() },
|
||||
});
|
||||
(getSecretLink as jest.Mock).mockResolvedValue({
|
||||
secretLink: "https://example.org/secret/initial",
|
||||
});
|
||||
clipboard: { writeText: jest.fn() }
|
||||
})
|
||||
;(getSecretLink as jest.Mock).mockResolvedValue({
|
||||
secretLink: 'https://example.org/secret/initial'
|
||||
})
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
@@ -342,79 +342,79 @@ describe("CalendarPopover - Tabs Scenarios", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ user: baseUser }
|
||||
);
|
||||
)
|
||||
|
||||
// Switch to Access tab
|
||||
fireEvent.click(screen.getByRole("tab", { name: /Access/i }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Access/i }))
|
||||
|
||||
// Expect text field with caldav link
|
||||
const input = screen.getByLabelText("calendar.caldav_access");
|
||||
expect(input).toHaveValue("https://cal.example.org/calendars/user1/cal1");
|
||||
const input = screen.getByLabelText('calendar.caldav_access')
|
||||
expect(input).toHaveValue('https://cal.example.org/calendars/user1/cal1')
|
||||
|
||||
// Click copy button (find button containing ContentCopyIcon)
|
||||
const copyIcon = screen.getAllByTestId("ContentCopyIcon")[0];
|
||||
const copyButton = copyIcon.closest("button");
|
||||
const copyIcon = screen.getAllByTestId('ContentCopyIcon')[0]
|
||||
const copyButton = copyIcon.closest('button')
|
||||
if (copyButton) {
|
||||
fireEvent.click(copyButton);
|
||||
fireEvent.click(copyButton)
|
||||
}
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
"https://cal.example.org/calendars/user1/cal1"
|
||||
);
|
||||
'https://cal.example.org/calendars/user1/cal1'
|
||||
)
|
||||
|
||||
// Snackbar should appear
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("common.link_copied")).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
expect(screen.getByText('common.link_copied')).toBeInTheDocument()
|
||||
)
|
||||
})
|
||||
|
||||
describe("Import flow", () => {
|
||||
const file = new File(["test"], "events.ics", { type: "text/calendar" });
|
||||
describe('Import flow', () => {
|
||||
const file = new File(['test'], 'events.ics', { type: 'text/calendar' })
|
||||
|
||||
it("creates a new calendar and imports events when Import with 'new' target", async () => {
|
||||
jest
|
||||
.spyOn(eventThunks, "createCalendarAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'createCalendarAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
jest
|
||||
.spyOn(eventThunks, "importEventFromFileAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'importEventFromFileAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarPopover open={true} onClose={mockOnClose} />,
|
||||
{
|
||||
user: baseUser,
|
||||
user: baseUser
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
// Switch to Import tab
|
||||
fireEvent.click(screen.getByRole("tab", { name: /Import/i }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Import/i }))
|
||||
|
||||
// Provide new calendar params
|
||||
fireEvent.change(screen.getByLabelText(/Name/i), {
|
||||
target: { value: "Imported Calendar" },
|
||||
});
|
||||
const fileInput = screen.getByLabelText("common.select_file");
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
target: { value: 'Imported Calendar' }
|
||||
})
|
||||
const fileInput = screen.getByLabelText('common.select_file')
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
// Click Import
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.import" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.import' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(eventThunks.createCalendarAsync).toHaveBeenCalled()
|
||||
);
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(eventThunks.importEventFromFileAsync).toHaveBeenCalled()
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("imports into an existing calendar when target is set", async () => {
|
||||
it('imports into an existing calendar when target is set', async () => {
|
||||
jest
|
||||
.spyOn(eventThunks, "importEventFromFileAsync")
|
||||
.mockImplementation(mockThunkWithUnwrap());
|
||||
.spyOn(eventThunks, 'importEventFromFileAsync')
|
||||
.mockImplementation(mockThunkWithUnwrap())
|
||||
|
||||
const calendars = {
|
||||
"user1/cal1": existingCalendar,
|
||||
};
|
||||
'user1/cal1': existingCalendar
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
@@ -423,51 +423,50 @@ describe("CalendarPopover - Tabs Scenarios", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ user: baseUser, calendars: { list: calendars } }
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: /Import/i }));
|
||||
const fileInput = screen.getByLabelText("common.select_file");
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Import/i }))
|
||||
const fileInput = screen.getByLabelText('common.select_file')
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.import" }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'actions.import' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(eventThunks.importEventFromFileAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
calLink: "/calendars/user1/cal1.json",
|
||||
file,
|
||||
calLink: '/calendars/user1/cal1.json',
|
||||
file
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("disables Import button until a file is uploaded", () => {
|
||||
it('disables Import button until a file is uploaded', () => {
|
||||
renderWithProviders(
|
||||
<CalendarPopover open={true} onClose={mockOnClose} />,
|
||||
{
|
||||
user: baseUser,
|
||||
user: baseUser
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: /Import/i }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Import/i }))
|
||||
|
||||
const importButton = screen.getByRole("button", {
|
||||
name: "actions.import",
|
||||
});
|
||||
expect(importButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
const importButton = screen.getByRole('button', {
|
||||
name: 'actions.import'
|
||||
})
|
||||
expect(importButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it("fetches and resets the secret link", async () => {
|
||||
window.DAV_BASE_URL = "https://cal.example.org";
|
||||
|
||||
(getSecretLink as jest.Mock)
|
||||
it('fetches and resets the secret link', async () => {
|
||||
window.DAV_BASE_URL = 'https://cal.example.org'
|
||||
;(getSecretLink as jest.Mock)
|
||||
.mockResolvedValueOnce({
|
||||
secretLink: "https://example.org/secret/initial",
|
||||
secretLink: 'https://example.org/secret/initial'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
secretLink: "https://example.org/secret/new",
|
||||
});
|
||||
secretLink: 'https://example.org/secret/new'
|
||||
})
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarPopover
|
||||
@@ -476,27 +475,27 @@ describe("CalendarPopover - Tabs Scenarios", () => {
|
||||
calendar={existingCalendar}
|
||||
/>,
|
||||
{ user: baseUser }
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: /Access/i }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Access/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByDisplayValue("https://example.org/secret/initial")
|
||||
screen.getByDisplayValue('https://example.org/secret/initial')
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /reset/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /reset/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByDisplayValue("https://example.org/secret/new")
|
||||
screen.getByDisplayValue('https://example.org/secret/new')
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
)
|
||||
|
||||
expect(getSecretLink).toHaveBeenCalledWith(
|
||||
existingCalendar.link.replace(".json", ""),
|
||||
existingCalendar.link.replace('.json', ''),
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as calAPI from "@/features/Calendars/CalendarApi";
|
||||
import * as calAPI from '@/features/Calendars/CalendarApi'
|
||||
import reducer, {
|
||||
addEvent,
|
||||
createCalendar,
|
||||
removeEvent,
|
||||
removeTempCal,
|
||||
updateEventLocal,
|
||||
} from "@/features/Calendars/CalendarSlice";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
updateEventLocal
|
||||
} from '@/features/Calendars/CalendarSlice'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import {
|
||||
addSharedCalendarAsync,
|
||||
createCalendarAsync,
|
||||
@@ -14,523 +14,523 @@ import {
|
||||
getCalendarsListAsync,
|
||||
getEventAsync,
|
||||
getTempCalendarsListAsync,
|
||||
patchACLCalendarAsync,
|
||||
} from "@/features/Calendars/services";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import * as userAPI from "@/features/User/userAPI";
|
||||
import userReducer, { setUserData } from "@/features/User/userSlice";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
patchACLCalendarAsync
|
||||
} from '@/features/Calendars/services'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import * as userAPI from '@/features/User/userAPI'
|
||||
import userReducer, { setUserData } from '@/features/User/userSlice'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock("@/features/User/userAPI");
|
||||
jest.mock("@/features/Events/EventApi");
|
||||
jest.mock("@/features/Events/utils");
|
||||
jest.mock("@/utils/apiUtils");
|
||||
jest.mock('@/features/Calendars/CalendarApi')
|
||||
jest.mock('@/features/User/userAPI')
|
||||
jest.mock('@/features/Events/EventApi')
|
||||
jest.mock('@/features/Events/utils')
|
||||
jest.mock('@/utils/apiUtils')
|
||||
|
||||
describe("CalendarSlice", () => {
|
||||
describe('CalendarSlice', () => {
|
||||
const initialState = {
|
||||
list: {},
|
||||
templist: {},
|
||||
pending: false,
|
||||
error: null,
|
||||
};
|
||||
error: null
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
describe("reducers", () => {
|
||||
it("createCalendar adds new calendar", () => {
|
||||
describe('reducers', () => {
|
||||
it('createCalendar adds new calendar', () => {
|
||||
const action = createCalendar({
|
||||
name: "Test Cal",
|
||||
color: "#ff0000",
|
||||
description: "desc",
|
||||
});
|
||||
const state = reducer(initialState, action);
|
||||
const values = Object.values(state.list);
|
||||
expect(values[0].name).toBe("Test Cal");
|
||||
expect(values[0].color).toBe("#ff0000");
|
||||
});
|
||||
name: 'Test Cal',
|
||||
color: '#ff0000',
|
||||
description: 'desc'
|
||||
})
|
||||
const state = reducer(initialState, action)
|
||||
const values = Object.values(state.list)
|
||||
expect(values[0].name).toBe('Test Cal')
|
||||
expect(values[0].color).toBe('#ff0000')
|
||||
})
|
||||
|
||||
it("addEvent adds event to calendar", () => {
|
||||
const calId = "user/cal";
|
||||
const event = { uid: "event1", title: "My Event" } as any;
|
||||
it('addEvent adds event to calendar', () => {
|
||||
const calId = 'user/cal'
|
||||
const event = { uid: 'event1', title: 'My Event' } as any
|
||||
const stateWithCal = {
|
||||
...initialState,
|
||||
list: { [calId]: { id: calId, events: {} } as any },
|
||||
};
|
||||
list: { [calId]: { id: calId, events: {} } as any }
|
||||
}
|
||||
const state = reducer(
|
||||
stateWithCal,
|
||||
addEvent({ calendarUid: calId, event })
|
||||
);
|
||||
expect(state.list[calId].events["event1"]).toEqual(
|
||||
expect.objectContaining({ uid: "event1" })
|
||||
);
|
||||
});
|
||||
)
|
||||
expect(state.list[calId].events['event1']).toEqual(
|
||||
expect.objectContaining({ uid: 'event1' })
|
||||
)
|
||||
})
|
||||
|
||||
it("removeEvent deletes event", () => {
|
||||
const calId = "user/cal";
|
||||
it('removeEvent deletes event', () => {
|
||||
const calId = 'user/cal'
|
||||
const stateWithEvent = {
|
||||
...initialState,
|
||||
list: {
|
||||
[calId]: {
|
||||
id: calId,
|
||||
events: { e1: { uid: "e1" } },
|
||||
} as unknown as Calendar,
|
||||
},
|
||||
};
|
||||
events: { e1: { uid: 'e1' } }
|
||||
} as unknown as Calendar
|
||||
}
|
||||
}
|
||||
const state = reducer(
|
||||
stateWithEvent,
|
||||
removeEvent({ calendarUid: calId, eventUid: "e1" })
|
||||
);
|
||||
expect(state.list[calId].events).toEqual({});
|
||||
});
|
||||
removeEvent({ calendarUid: calId, eventUid: 'e1' })
|
||||
)
|
||||
expect(state.list[calId].events).toEqual({})
|
||||
})
|
||||
|
||||
it("updateEventLocal updates an event", () => {
|
||||
const calId = "user/cal";
|
||||
it('updateEventLocal updates an event', () => {
|
||||
const calId = 'user/cal'
|
||||
const stateWithEvent = {
|
||||
...initialState,
|
||||
list: {
|
||||
[calId]: {
|
||||
id: calId,
|
||||
events: { e1: { uid: "e1", title: "Old" } },
|
||||
} as unknown as Calendar,
|
||||
},
|
||||
};
|
||||
events: { e1: { uid: 'e1', title: 'Old' } }
|
||||
} as unknown as Calendar
|
||||
}
|
||||
}
|
||||
const state = reducer(
|
||||
stateWithEvent,
|
||||
updateEventLocal({ calId, event: { uid: "e1", title: "New" } as any })
|
||||
);
|
||||
expect(state.list[calId].events.e1.title).toBe("New");
|
||||
});
|
||||
updateEventLocal({ calId, event: { uid: 'e1', title: 'New' } as any })
|
||||
)
|
||||
expect(state.list[calId].events.e1.title).toBe('New')
|
||||
})
|
||||
|
||||
it("removeTempCal deletes temp calendar", () => {
|
||||
it('removeTempCal deletes temp calendar', () => {
|
||||
const stateWithTemp = {
|
||||
...initialState,
|
||||
templist: { temp1: { id: "temp1" } as any },
|
||||
};
|
||||
const state = reducer(stateWithTemp, removeTempCal("temp1"));
|
||||
expect(state.templist).toEqual({});
|
||||
});
|
||||
});
|
||||
templist: { temp1: { id: 'temp1' } as any }
|
||||
}
|
||||
const state = reducer(stateWithTemp, removeTempCal('temp1'))
|
||||
expect(state.templist).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("extraReducers (thunks)", () => {
|
||||
describe('extraReducers (thunks)', () => {
|
||||
const storeFactory = () =>
|
||||
configureStore({
|
||||
reducer: { calendars: reducer, user: userReducer },
|
||||
});
|
||||
reducer: { calendars: reducer, user: userReducer }
|
||||
})
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it("getCalendarsListAsync.fulfilled replaces list", async () => {
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": [] },
|
||||
});
|
||||
(userAPI.getUserDetails as jest.Mock).mockResolvedValue({
|
||||
firstname: "Alice",
|
||||
lastname: "Smith",
|
||||
emails: ["a@b.com"],
|
||||
});
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
it('getCalendarsListAsync.fulfilled replaces list', async () => {
|
||||
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
|
||||
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { 'dav:calendar': [] }
|
||||
})
|
||||
;(userAPI.getUserDetails as jest.Mock).mockResolvedValue({
|
||||
firstname: 'Alice',
|
||||
lastname: 'Smith',
|
||||
emails: ['a@b.com']
|
||||
})
|
||||
|
||||
const store = storeFactory();
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
const state = store.getState().calendars;
|
||||
expect(state.list).toEqual({});
|
||||
});
|
||||
const store = storeFactory()
|
||||
await store.dispatch(getCalendarsListAsync() as any)
|
||||
const state = store.getState().calendars
|
||||
expect(state.list).toEqual({})
|
||||
})
|
||||
|
||||
it("getCalendarsListAsync loads user details in parallel for multiple owners", async () => {
|
||||
it('getCalendarsListAsync loads user details in parallel for multiple owners', async () => {
|
||||
const mockCalendars = [
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||
"dav:name": "Calendar 1",
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
_links: { self: { href: '/calendars/u1/cal1.json' } },
|
||||
'dav:name': 'Calendar 1',
|
||||
'apple:color': '#FF0000',
|
||||
acl: []
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u2/cal2.json" } },
|
||||
"dav:name": "Calendar 2",
|
||||
"apple:color": "#00FF00",
|
||||
acl: [],
|
||||
_links: { self: { href: '/calendars/u2/cal2.json' } },
|
||||
'dav:name': 'Calendar 2',
|
||||
'apple:color': '#00FF00',
|
||||
acl: []
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u3/cal3.json" } },
|
||||
"dav:name": "Calendar 3",
|
||||
"apple:color": "#0000FF",
|
||||
acl: [],
|
||||
},
|
||||
];
|
||||
_links: { self: { href: '/calendars/u3/cal3.json' } },
|
||||
'dav:name': 'Calendar 3',
|
||||
'apple:color': '#0000FF',
|
||||
acl: []
|
||||
}
|
||||
]
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
|
||||
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { 'dav:calendar': mockCalendars }
|
||||
})
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
|
||||
getUserDetailsMock
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Alice",
|
||||
lastname: "Smith",
|
||||
emails: ["alice@example.com"],
|
||||
firstname: 'Alice',
|
||||
lastname: 'Smith',
|
||||
emails: ['alice@example.com']
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Bob",
|
||||
lastname: "Jones",
|
||||
emails: ["bob@example.com"],
|
||||
firstname: 'Bob',
|
||||
lastname: 'Jones',
|
||||
emails: ['bob@example.com']
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Charlie",
|
||||
lastname: "Brown",
|
||||
emails: ["charlie@example.com"],
|
||||
});
|
||||
firstname: 'Charlie',
|
||||
lastname: 'Brown',
|
||||
emails: ['charlie@example.com']
|
||||
})
|
||||
|
||||
const store = storeFactory();
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
const store = storeFactory()
|
||||
await store.dispatch(getCalendarsListAsync() as any)
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(3);
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u1");
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u2");
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u3");
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(3)
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith('u1')
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith('u2')
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith('u3')
|
||||
|
||||
const state = store.getState().calendars;
|
||||
expect(state.list["u1/cal1"].owner.firstname).toContain("Alice");
|
||||
expect(state.list["u2/cal2"].owner.firstname).toContain("Bob");
|
||||
expect(state.list["u3/cal3"].owner.firstname).toContain("Charlie");
|
||||
});
|
||||
const state = store.getState().calendars
|
||||
expect(state.list['u1/cal1'].owner.firstname).toContain('Alice')
|
||||
expect(state.list['u2/cal2'].owner.firstname).toContain('Bob')
|
||||
expect(state.list['u3/cal3'].owner.firstname).toContain('Charlie')
|
||||
})
|
||||
|
||||
it("getCalendarsListAsync deduplicates getUserDetails calls for same ownerId", async () => {
|
||||
it('getCalendarsListAsync deduplicates getUserDetails calls for same ownerId', async () => {
|
||||
const mockCalendars = [
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||
"dav:name": "Calendar 1",
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
_links: { self: { href: '/calendars/u1/cal1.json' } },
|
||||
'dav:name': 'Calendar 1',
|
||||
'apple:color': '#FF0000',
|
||||
acl: []
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal2.json" } },
|
||||
"dav:name": "Calendar 2",
|
||||
"apple:color": "#00FF00",
|
||||
acl: [],
|
||||
_links: { self: { href: '/calendars/u1/cal2.json' } },
|
||||
'dav:name': 'Calendar 2',
|
||||
'apple:color': '#00FF00',
|
||||
acl: []
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal3.json" } },
|
||||
"dav:name": "Calendar 3",
|
||||
"apple:color": "#0000FF",
|
||||
acl: [],
|
||||
},
|
||||
];
|
||||
_links: { self: { href: '/calendars/u1/cal3.json' } },
|
||||
'dav:name': 'Calendar 3',
|
||||
'apple:color': '#0000FF',
|
||||
acl: []
|
||||
}
|
||||
]
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
|
||||
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { 'dav:calendar': mockCalendars }
|
||||
})
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
|
||||
getUserDetailsMock.mockResolvedValue({
|
||||
firstname: "Alice",
|
||||
lastname: "Smith",
|
||||
emails: ["alice@example.com"],
|
||||
});
|
||||
firstname: 'Alice',
|
||||
lastname: 'Smith',
|
||||
emails: ['alice@example.com']
|
||||
})
|
||||
|
||||
const store = storeFactory();
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
const store = storeFactory()
|
||||
await store.dispatch(getCalendarsListAsync() as any)
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u1");
|
||||
});
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(1)
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith('u1')
|
||||
})
|
||||
|
||||
it("getCalendarsListAsync processes owners in batches of 20", async () => {
|
||||
it('getCalendarsListAsync processes owners in batches of 20', async () => {
|
||||
const mockCalendars = Array.from({ length: 45 }, (_, i) => ({
|
||||
_links: { self: { href: `/calendars/u${i + 1}/cal${i + 1}.json` } },
|
||||
"dav:name": `Calendar ${i + 1}`,
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
}));
|
||||
'dav:name': `Calendar ${i + 1}`,
|
||||
'apple:color': '#FF0000',
|
||||
acl: []
|
||||
}))
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
|
||||
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { 'dav:calendar': mockCalendars }
|
||||
})
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
|
||||
getUserDetailsMock.mockImplementation((ownerId: string) =>
|
||||
Promise.resolve({
|
||||
firstname: "User",
|
||||
firstname: 'User',
|
||||
lastname: ownerId,
|
||||
emails: [`${ownerId}@example.com`],
|
||||
emails: [`${ownerId}@example.com`]
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
const store = storeFactory();
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
const store = storeFactory()
|
||||
await store.dispatch(getCalendarsListAsync() as any)
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(45);
|
||||
const state = store.getState().calendars;
|
||||
expect(Object.keys(state.list)).toHaveLength(45);
|
||||
});
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(45)
|
||||
const state = store.getState().calendars
|
||||
expect(Object.keys(state.list)).toHaveLength(45)
|
||||
})
|
||||
|
||||
it("getCalendarsListAsync doesnt call getUserDetails if userdata exist in store", async () => {
|
||||
it('getCalendarsListAsync doesnt call getUserDetails if userdata exist in store', async () => {
|
||||
const existingCalendars = {
|
||||
"u1/cal1": {
|
||||
id: "u1/cal1",
|
||||
name: "Existing Calendar",
|
||||
events: {},
|
||||
} as Calendar,
|
||||
};
|
||||
'u1/cal1': {
|
||||
id: 'u1/cal1',
|
||||
name: 'Existing Calendar',
|
||||
events: {}
|
||||
} as Calendar
|
||||
}
|
||||
|
||||
const store = storeFactory();
|
||||
const store = storeFactory()
|
||||
store.dispatch({
|
||||
type: "calendars/getCalendars/fulfilled",
|
||||
payload: { importedCalendars: existingCalendars, errors: "" },
|
||||
});
|
||||
store.dispatch(setUserData({ openpaasId: "bla" }));
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
type: 'calendars/getCalendars/fulfilled',
|
||||
payload: { importedCalendars: existingCalendars, errors: '' }
|
||||
})
|
||||
store.dispatch(setUserData({ openpaasId: 'bla' }))
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
|
||||
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
await store.dispatch(getCalendarsListAsync() as any)
|
||||
|
||||
expect(getUserDetailsMock).not.toHaveBeenCalled();
|
||||
expect(getUserDetailsMock).not.toHaveBeenCalled()
|
||||
|
||||
const state = store.getState().calendars;
|
||||
expect(state.list).toEqual(existingCalendars);
|
||||
});
|
||||
const state = store.getState().calendars
|
||||
expect(state.list).toEqual(existingCalendars)
|
||||
})
|
||||
|
||||
it("getCalendarsListAsync handles errors in getUserDetails gracefully", async () => {
|
||||
it('getCalendarsListAsync handles errors in getUserDetails gracefully', async () => {
|
||||
const mockCalendars = [
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||
"dav:name": "Calendar 1",
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
_links: { self: { href: '/calendars/u1/cal1.json' } },
|
||||
'dav:name': 'Calendar 1',
|
||||
'apple:color': '#FF0000',
|
||||
acl: []
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u2/cal2.json" } },
|
||||
"dav:name": "Calendar 2",
|
||||
"apple:color": "#00FF00",
|
||||
acl: [],
|
||||
},
|
||||
];
|
||||
_links: { self: { href: '/calendars/u2/cal2.json' } },
|
||||
'dav:name': 'Calendar 2',
|
||||
'apple:color': '#00FF00',
|
||||
acl: []
|
||||
}
|
||||
]
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
;(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: 'u1' })
|
||||
;(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { 'dav:calendar': mockCalendars }
|
||||
})
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock
|
||||
getUserDetailsMock
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Alice",
|
||||
lastname: "Smith",
|
||||
emails: ["alice@example.com"],
|
||||
firstname: 'Alice',
|
||||
lastname: 'Smith',
|
||||
emails: ['alice@example.com']
|
||||
})
|
||||
.mockRejectedValueOnce(new Error("Failed to fetch user"));
|
||||
.mockRejectedValueOnce(new Error('Failed to fetch user'))
|
||||
|
||||
const store = storeFactory();
|
||||
const result = await store.dispatch(getCalendarsListAsync() as any);
|
||||
const store = storeFactory()
|
||||
const result = await store.dispatch(getCalendarsListAsync() as any)
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(2);
|
||||
const state = store.getState().calendars;
|
||||
expect(state.list["u1/cal1"].owner.firstname).toContain("Alice");
|
||||
expect(state.list["u2/cal2"].owner.lastname).toContain("Unknown User");
|
||||
expect(result.payload.errors).toBeTruthy();
|
||||
});
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(2)
|
||||
const state = store.getState().calendars
|
||||
expect(state.list['u1/cal1'].owner.firstname).toContain('Alice')
|
||||
expect(state.list['u2/cal2'].owner.lastname).toContain('Unknown User')
|
||||
expect(result.payload.errors).toBeTruthy()
|
||||
})
|
||||
|
||||
it("patchACLCalendarAsync.fulfilled sets visibility", () => {
|
||||
it('patchACLCalendarAsync.fulfilled sets visibility', () => {
|
||||
const prev = {
|
||||
...initialState,
|
||||
list: { c1: { id: "c1", visibility: "public" } as any },
|
||||
};
|
||||
list: { c1: { id: 'c1', visibility: 'public' } as any }
|
||||
}
|
||||
const state = reducer(
|
||||
prev,
|
||||
patchACLCalendarAsync.fulfilled(
|
||||
{ calId: "c1", calLink: "l", request: "" },
|
||||
"req3",
|
||||
{ calId: "c1", calLink: "l", request: "" }
|
||||
{ calId: 'c1', calLink: 'l', request: '' },
|
||||
'req3',
|
||||
{ calId: 'c1', calLink: 'l', request: '' }
|
||||
)
|
||||
);
|
||||
expect(state.list.c1.visibility).toBe("private");
|
||||
});
|
||||
)
|
||||
expect(state.list.c1.visibility).toBe('private')
|
||||
})
|
||||
|
||||
it("createCalendarAsync.fulfilled adds a new calendar", () => {
|
||||
it('createCalendarAsync.fulfilled adds a new calendar', () => {
|
||||
const payload = {
|
||||
userData: {
|
||||
openpaasId: "u1",
|
||||
family_name: "Owner",
|
||||
email: "o@example.com",
|
||||
sid: "",
|
||||
sub: "",
|
||||
given_name: "Test",
|
||||
name: "Test Owner",
|
||||
openpaasId: 'u1',
|
||||
family_name: 'Owner',
|
||||
email: 'o@example.com',
|
||||
sid: '',
|
||||
sub: '',
|
||||
given_name: 'Test',
|
||||
name: 'Test Owner'
|
||||
},
|
||||
calId: "cal1",
|
||||
color: { "apple:color": "#f00" },
|
||||
name: "Test",
|
||||
desc: "Desc",
|
||||
};
|
||||
calId: 'cal1',
|
||||
color: { 'apple:color': '#f00' },
|
||||
name: 'Test',
|
||||
desc: 'Desc'
|
||||
}
|
||||
|
||||
const payloadResponse = {
|
||||
userId: "u1",
|
||||
calId: "cal1",
|
||||
color: { "apple:color": "#f00" },
|
||||
name: "Test",
|
||||
desc: "Desc",
|
||||
owner: { firstname: "Owner", emails: ["o@example.com"] },
|
||||
};
|
||||
userId: 'u1',
|
||||
calId: 'cal1',
|
||||
color: { 'apple:color': '#f00' },
|
||||
name: 'Test',
|
||||
desc: 'Desc',
|
||||
owner: { firstname: 'Owner', emails: ['o@example.com'] }
|
||||
}
|
||||
const state = reducer(
|
||||
initialState,
|
||||
createCalendarAsync.fulfilled(payloadResponse, "req4", payload)
|
||||
);
|
||||
expect(state.list["u1/cal1"].name).toBe("Test");
|
||||
expect(state.list["u1/cal1"].color?.["apple:color"]).toBe("#f00");
|
||||
});
|
||||
createCalendarAsync.fulfilled(payloadResponse, 'req4', payload)
|
||||
)
|
||||
expect(state.list['u1/cal1'].name).toBe('Test')
|
||||
expect(state.list['u1/cal1'].color?.['apple:color']).toBe('#f00')
|
||||
})
|
||||
|
||||
it("addSharedCalendarAsync.fulfilled adds shared calendar", () => {
|
||||
it('addSharedCalendarAsync.fulfilled adds shared calendar', () => {
|
||||
const payload = {
|
||||
calId: "c1",
|
||||
color: { "apple:color": "#0f0" },
|
||||
link: "/calendars/u1/c1.json",
|
||||
name: "Shared",
|
||||
desc: "Shared Desc",
|
||||
owner: { firstname: "O", emails: ["o@example.com"] },
|
||||
};
|
||||
calId: 'c1',
|
||||
color: { 'apple:color': '#0f0' },
|
||||
link: '/calendars/u1/c1.json',
|
||||
name: 'Shared',
|
||||
desc: 'Shared Desc',
|
||||
owner: { firstname: 'O', emails: ['o@example.com'] }
|
||||
}
|
||||
const mockCal = {
|
||||
cal: {
|
||||
_links: { self: { href: "/calendars/u1/c1.json" } },
|
||||
"apple:color": "#0f0",
|
||||
"caldav:description": "Shared Desc",
|
||||
"dav:name": "Shared",
|
||||
},
|
||||
};
|
||||
_links: { self: { href: '/calendars/u1/c1.json' } },
|
||||
'apple:color': '#0f0',
|
||||
'caldav:description': 'Shared Desc',
|
||||
'dav:name': 'Shared'
|
||||
}
|
||||
}
|
||||
const state = reducer(
|
||||
initialState,
|
||||
addSharedCalendarAsync.fulfilled(payload, "req5", {
|
||||
userId: "u1",
|
||||
calId: "c1",
|
||||
cal: mockCal,
|
||||
addSharedCalendarAsync.fulfilled(payload, 'req5', {
|
||||
userId: 'u1',
|
||||
calId: 'c1',
|
||||
cal: mockCal
|
||||
})
|
||||
);
|
||||
expect(state.list["c1"].name).toBe("Shared");
|
||||
});
|
||||
)
|
||||
expect(state.list['c1'].name).toBe('Shared')
|
||||
})
|
||||
|
||||
it("getTempCalendarsListAsync.fulfilled updates templist", () => {
|
||||
it('getTempCalendarsListAsync.fulfilled updates templist', () => {
|
||||
const payload = {
|
||||
t1: {
|
||||
id: "t1",
|
||||
name: "Temp",
|
||||
color: { "apple:color": "#aaa" },
|
||||
id: 't1',
|
||||
name: 'Temp',
|
||||
color: { 'apple:color': '#aaa' },
|
||||
events: {},
|
||||
visibility: "public",
|
||||
owner: { firstname: "O", emails: ["o@o.com"] },
|
||||
link: "/calendars/t1.json",
|
||||
description: "desc",
|
||||
} as Calendar,
|
||||
};
|
||||
visibility: 'public',
|
||||
owner: { firstname: 'O', emails: ['o@o.com'] },
|
||||
link: '/calendars/t1.json',
|
||||
description: 'desc'
|
||||
} as Calendar
|
||||
}
|
||||
const state = reducer(
|
||||
initialState,
|
||||
getTempCalendarsListAsync.fulfilled(payload, "req7", {
|
||||
openpaasId: "u1",
|
||||
color: { "apple:color": "#aaa" },
|
||||
displayName: "test",
|
||||
avatarUrl: "",
|
||||
email: "test@test.com",
|
||||
getTempCalendarsListAsync.fulfilled(payload, 'req7', {
|
||||
openpaasId: 'u1',
|
||||
color: { 'apple:color': '#aaa' },
|
||||
displayName: 'test',
|
||||
avatarUrl: '',
|
||||
email: 'test@test.com'
|
||||
})
|
||||
);
|
||||
expect(state.templist.t1.name).toBe("Temp");
|
||||
});
|
||||
)
|
||||
expect(state.templist.t1.name).toBe('Temp')
|
||||
})
|
||||
|
||||
it("getEventAsync.fulfilled adds single event", () => {
|
||||
const payload = { calId: "c1", event: { uid: "e1" } as any };
|
||||
it('getEventAsync.fulfilled adds single event', () => {
|
||||
const payload = { calId: 'c1', event: { uid: 'e1' } as any }
|
||||
const state = reducer(
|
||||
initialState,
|
||||
getEventAsync.fulfilled(payload, "req9", { uid: "e1" } as any)
|
||||
);
|
||||
expect(state.list.c1.events.e1.uid).toBe("e1");
|
||||
});
|
||||
getEventAsync.fulfilled(payload, 'req9', { uid: 'e1' } as any)
|
||||
)
|
||||
expect(state.list.c1.events.e1.uid).toBe('e1')
|
||||
})
|
||||
|
||||
it("getCalendarDetailAsync.fulfilled adds calendar events", () => {
|
||||
const payload = { calId: "c1", events: [{ uid: "e1" }] as any[] };
|
||||
it('getCalendarDetailAsync.fulfilled adds calendar events', () => {
|
||||
const payload = { calId: 'c1', events: [{ uid: 'e1' }] as any[] }
|
||||
const state = reducer(
|
||||
{
|
||||
...initialState,
|
||||
list: {
|
||||
["c1"]: {
|
||||
id: "c1",
|
||||
events: {},
|
||||
} as unknown as Calendar,
|
||||
},
|
||||
['c1']: {
|
||||
id: 'c1',
|
||||
events: {}
|
||||
} as unknown as Calendar
|
||||
}
|
||||
},
|
||||
getCalendarDetailAsync.fulfilled(payload, "req11", {
|
||||
calId: "c1",
|
||||
match: { start: "", end: "" },
|
||||
getCalendarDetailAsync.fulfilled(payload, 'req11', {
|
||||
calId: 'c1',
|
||||
match: { start: '', end: '' }
|
||||
})
|
||||
);
|
||||
expect(state.list.c1.events.e1.uid).toBe("e1");
|
||||
});
|
||||
)
|
||||
expect(state.list.c1.events.e1.uid).toBe('e1')
|
||||
})
|
||||
|
||||
it("getEventAsync.fulfilled doesnt create new events when there are already event with base UID", () => {
|
||||
const baseUid = "recurring-event-base";
|
||||
it('getEventAsync.fulfilled doesnt create new events when there are already event with base UID', () => {
|
||||
const baseUid = 'recurring-event-base'
|
||||
const existingEvent = {
|
||||
uid: `${baseUid}/20240115`,
|
||||
title: "Existing Recurring Event",
|
||||
recurrenceId: null,
|
||||
} as unknown as CalendarEvent;
|
||||
title: 'Existing Recurring Event',
|
||||
recurrenceId: null
|
||||
} as unknown as CalendarEvent
|
||||
|
||||
const newEventInstance = {
|
||||
uid: baseUid,
|
||||
title: "Fetched Master Event",
|
||||
recurrenceId: "20240115",
|
||||
} as CalendarEvent;
|
||||
title: 'Fetched Master Event',
|
||||
recurrenceId: '20240115'
|
||||
} as CalendarEvent
|
||||
|
||||
const stateWithEvent = {
|
||||
...initialState,
|
||||
list: {
|
||||
c1: {
|
||||
id: "c1",
|
||||
events: { [`${baseUid}/20240115`]: existingEvent },
|
||||
} as unknown as Calendar,
|
||||
},
|
||||
};
|
||||
id: 'c1',
|
||||
events: { [`${baseUid}/20240115`]: existingEvent }
|
||||
} as unknown as Calendar
|
||||
}
|
||||
}
|
||||
|
||||
const payload = { calId: "c1", event: newEventInstance };
|
||||
const payload = { calId: 'c1', event: newEventInstance }
|
||||
const state = reducer(
|
||||
stateWithEvent,
|
||||
getEventAsync.fulfilled(payload, "req", newEventInstance)
|
||||
);
|
||||
getEventAsync.fulfilled(payload, 'req', newEventInstance)
|
||||
)
|
||||
|
||||
// Should still only have the base event, not the instance
|
||||
expect(Object.keys(state.list.c1.events)).toHaveLength(1);
|
||||
expect(state.list.c1.events[`${baseUid}/20240115`]).toBeDefined();
|
||||
expect(state.list.c1.events[baseUid]).toBeUndefined();
|
||||
});
|
||||
expect(Object.keys(state.list.c1.events)).toHaveLength(1)
|
||||
expect(state.list.c1.events[`${baseUid}/20240115`]).toBeDefined()
|
||||
expect(state.list.c1.events[baseUid]).toBeUndefined()
|
||||
})
|
||||
|
||||
it("getEventAsync.fulfilled create new event when there isn't any event with base UID", () => {
|
||||
const eventUid = "new-event-uid";
|
||||
const eventUid = 'new-event-uid'
|
||||
const newEvent = {
|
||||
uid: eventUid,
|
||||
title: "New Event",
|
||||
recurrenceId: null,
|
||||
} as unknown as CalendarEvent;
|
||||
title: 'New Event',
|
||||
recurrenceId: null
|
||||
} as unknown as CalendarEvent
|
||||
|
||||
const stateWithoutEvent = {
|
||||
...initialState,
|
||||
list: {
|
||||
c1: {
|
||||
id: "c1",
|
||||
events: {},
|
||||
} as unknown as Calendar,
|
||||
},
|
||||
};
|
||||
id: 'c1',
|
||||
events: {}
|
||||
} as unknown as Calendar
|
||||
}
|
||||
}
|
||||
|
||||
const payload = { calId: "c1", event: newEvent };
|
||||
const payload = { calId: 'c1', event: newEvent }
|
||||
const state = reducer(
|
||||
stateWithoutEvent,
|
||||
getEventAsync.fulfilled(payload, "req", newEvent)
|
||||
);
|
||||
getEventAsync.fulfilled(payload, 'req', newEvent)
|
||||
)
|
||||
|
||||
// Should create the new event
|
||||
expect(Object.keys(state.list.c1.events)).toHaveLength(1);
|
||||
expect(state.list.c1.events[eventUid]).toBeDefined();
|
||||
expect(state.list.c1.events[eventUid].uid).toBe(eventUid);
|
||||
expect(state.list.c1.events[eventUid].title).toBe("New Event");
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(Object.keys(state.list.c1.events)).toHaveLength(1)
|
||||
expect(state.list.c1.events[eventUid]).toBeDefined()
|
||||
expect(state.list.c1.events[eventUid].uid).toBe(eventUid)
|
||||
expect(state.list.c1.events[eventUid].title).toBe('New Event')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,196 +1,196 @@
|
||||
import CalendarApp from "@/components/Calendar/Calendar";
|
||||
import * as calendarUtils from "@/components/Calendar/utils/calendarUtils";
|
||||
import { updateSlotLabelVisibility } from "@/components/Calendar/utils/calendarUtils";
|
||||
import EventPreviewModal from "@/features/Events/EventPreview";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import * as SettingsSlice from "@/features/Settings/SettingsSlice";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import CalendarApp from '@/components/Calendar/Calendar'
|
||||
import * as calendarUtils from '@/components/Calendar/utils/calendarUtils'
|
||||
import { updateSlotLabelVisibility } from '@/components/Calendar/utils/calendarUtils'
|
||||
import EventPreviewModal from '@/features/Events/EventPreview'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import * as SettingsSlice from '@/features/Settings/SettingsSlice'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
describe("Calendar - Timezone Integration", () => {
|
||||
const mockCalendarRef = { current: null };
|
||||
describe('Calendar - Timezone Integration', () => {
|
||||
const mockCalendarRef = { current: null }
|
||||
|
||||
const baseState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "testSid",
|
||||
openpaasId: "user1",
|
||||
},
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'testSid',
|
||||
openpaasId: 'user1'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
id: "user1/cal1",
|
||||
link: "/calendars/user1/cal1.json",
|
||||
name: "Test Calendar",
|
||||
description: "",
|
||||
color: "#33B679",
|
||||
owner: { firstname: "user1", emails: ["test@test.com"] },
|
||||
visibility: "public",
|
||||
events: {},
|
||||
},
|
||||
'user1/cal1': {
|
||||
id: 'user1/cal1',
|
||||
link: '/calendars/user1/cal1.json',
|
||||
name: 'Test Calendar',
|
||||
description: '',
|
||||
color: '#33B679',
|
||||
owner: { firstname: 'user1', emails: ['test@test.com'] },
|
||||
visibility: 'public',
|
||||
events: {}
|
||||
}
|
||||
},
|
||||
timeZone: "America/New_York",
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
timeZone: 'America/New_York',
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders TimezoneSelector in week view", async () => {
|
||||
it('renders TimezoneSelector in week view', async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
baseState
|
||||
);
|
||||
)
|
||||
|
||||
// Look for timezone selector button (should show offset)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/UTC/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText(/UTC/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("dispatches setTimeZone action when timezone is changed", async () => {
|
||||
const setTimeZoneSpy = jest.spyOn(SettingsSlice, "setTimeZone");
|
||||
it('dispatches setTimeZone action when timezone is changed', async () => {
|
||||
const setTimeZoneSpy = jest.spyOn(SettingsSlice, 'setTimeZone')
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
baseState
|
||||
);
|
||||
)
|
||||
|
||||
// Find and click timezone selector
|
||||
await waitFor(() => {
|
||||
const timezoneButton = screen.getByText(/UTC/i);
|
||||
fireEvent.click(timezoneButton);
|
||||
const timezoneButton = screen.getByText(/UTC/i)
|
||||
fireEvent.click(timezoneButton)
|
||||
|
||||
// Select a different timezone
|
||||
const autocomplete = screen.getByRole("combobox");
|
||||
fireEvent.change(autocomplete, { target: { value: "Tokyo" } });
|
||||
});
|
||||
const option = await screen.findByText(/Tokyo/i);
|
||||
fireEvent.click(option);
|
||||
const autocomplete = screen.getByRole('combobox')
|
||||
fireEvent.change(autocomplete, { target: { value: 'Tokyo' } })
|
||||
})
|
||||
const option = await screen.findByText(/Tokyo/i)
|
||||
fireEvent.click(option)
|
||||
|
||||
expect(setTimeZoneSpy).toHaveBeenCalledWith("Asia/Tokyo");
|
||||
});
|
||||
});
|
||||
expect(setTimeZoneSpy).toHaveBeenCalledWith('Asia/Tokyo')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Calendar - Timezone Slot Label Visibility", () => {
|
||||
it("hides slot labels within 15 minutes of current time", () => {
|
||||
const currentTime = new Date("2025-01-15T14:30:00Z");
|
||||
const timezone = "UTC";
|
||||
describe('Calendar - Timezone Slot Label Visibility', () => {
|
||||
it('hides slot labels within 15 minutes of current time', () => {
|
||||
const currentTime = new Date('2025-01-15T14:30:00Z')
|
||||
const timezone = 'UTC'
|
||||
jest
|
||||
.spyOn(calendarUtils, "checkIfCurrentWeekOrDay")
|
||||
.mockImplementation(() => true);
|
||||
.spyOn(calendarUtils, 'checkIfCurrentWeekOrDay')
|
||||
.mockImplementation(() => true)
|
||||
|
||||
// 14:25 - within 15 minutes
|
||||
const slot1425 = { text: "14:25" };
|
||||
const slot1425 = { text: '14:25' }
|
||||
expect(updateSlotLabelVisibility(currentTime, slot1425, timezone)).toBe(
|
||||
"timegrid-slot-label-hidden"
|
||||
);
|
||||
'timegrid-slot-label-hidden'
|
||||
)
|
||||
|
||||
// 14:30 - exact match
|
||||
const slot1430 = { text: "14:30" };
|
||||
const slot1430 = { text: '14:30' }
|
||||
expect(updateSlotLabelVisibility(currentTime, slot1430, timezone)).toBe(
|
||||
"timegrid-slot-label-hidden"
|
||||
);
|
||||
'timegrid-slot-label-hidden'
|
||||
)
|
||||
|
||||
// 14:35 - within 15 minutes
|
||||
const slot1435 = { text: "14:35" };
|
||||
const slot1435 = { text: '14:35' }
|
||||
expect(updateSlotLabelVisibility(currentTime, slot1435, timezone)).toBe(
|
||||
"timegrid-slot-label-hidden"
|
||||
);
|
||||
});
|
||||
'timegrid-slot-label-hidden'
|
||||
)
|
||||
})
|
||||
|
||||
it("shows slot labels more than 15 minutes from current time", () => {
|
||||
it('shows slot labels more than 15 minutes from current time', () => {
|
||||
jest
|
||||
.spyOn(calendarUtils, "checkIfCurrentWeekOrDay")
|
||||
.mockImplementation(() => true);
|
||||
.spyOn(calendarUtils, 'checkIfCurrentWeekOrDay')
|
||||
.mockImplementation(() => true)
|
||||
|
||||
const currentTime = new Date("2025-01-15T14:30:00");
|
||||
const timezone = "UTC";
|
||||
const currentTime = new Date('2025-01-15T14:30:00')
|
||||
const timezone = 'UTC'
|
||||
|
||||
// 14:00 - 30 minutes before
|
||||
const slot1400 = { text: "14:00" };
|
||||
const slot1400 = { text: '14:00' }
|
||||
expect(updateSlotLabelVisibility(currentTime, slot1400, timezone)).toBe(
|
||||
"fc-timegrid-slot-label"
|
||||
);
|
||||
'fc-timegrid-slot-label'
|
||||
)
|
||||
|
||||
// 15:00 - 30 minutes after
|
||||
const slot1500 = { text: "15:00" };
|
||||
const slot1500 = { text: '15:00' }
|
||||
expect(updateSlotLabelVisibility(currentTime, slot1500, timezone)).toBe(
|
||||
"fc-timegrid-slot-label"
|
||||
);
|
||||
});
|
||||
'fc-timegrid-slot-label'
|
||||
)
|
||||
})
|
||||
|
||||
it("returns visible class when not in current week/day", () => {
|
||||
it('returns visible class when not in current week/day', () => {
|
||||
jest
|
||||
.spyOn(calendarUtils, "checkIfCurrentWeekOrDay")
|
||||
.mockImplementation(() => false);
|
||||
const currentTime = new Date("2025-01-15T14:30:00");
|
||||
const timezone = "UTC";
|
||||
const slot = { text: "14:30" };
|
||||
.spyOn(calendarUtils, 'checkIfCurrentWeekOrDay')
|
||||
.mockImplementation(() => false)
|
||||
const currentTime = new Date('2025-01-15T14:30:00')
|
||||
const timezone = 'UTC'
|
||||
const slot = { text: '14:30' }
|
||||
|
||||
// Should return visible class when not in current view
|
||||
const result = updateSlotLabelVisibility(currentTime, slot, timezone);
|
||||
expect(result).toBe("fc-timegrid-slot-label");
|
||||
});
|
||||
});
|
||||
const result = updateSlotLabelVisibility(currentTime, slot, timezone)
|
||||
expect(result).toBe('fc-timegrid-slot-label')
|
||||
})
|
||||
})
|
||||
|
||||
describe("EventDisplayPreview - Timezone Display", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
describe('EventDisplayPreview - Timezone Display', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
const baseEvent: CalendarEvent = {
|
||||
uid: "event1",
|
||||
title: "Team Meeting",
|
||||
start: new Date("2025-01-15T14:00:00Z"),
|
||||
end: new Date("2025-01-15T15:00:00Z"),
|
||||
calendarId: "user1/cal1",
|
||||
allday: false,
|
||||
};
|
||||
uid: 'event1',
|
||||
title: 'Team Meeting',
|
||||
start: new Date('2025-01-15T14:00:00Z'),
|
||||
end: new Date('2025-01-15T15:00:00Z'),
|
||||
calendarId: 'user1/cal1',
|
||||
allday: false
|
||||
}
|
||||
|
||||
const allDayEvent = {
|
||||
...baseEvent,
|
||||
allday: true,
|
||||
start: new Date("2025-01-15"),
|
||||
end: new Date("2025-01-16"),
|
||||
};
|
||||
start: new Date('2025-01-15'),
|
||||
end: new Date('2025-01-16')
|
||||
}
|
||||
|
||||
const baseState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "testSid",
|
||||
openpaasId: "user1",
|
||||
},
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'testSid',
|
||||
openpaasId: 'user1'
|
||||
}
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
id: "user1/cal1",
|
||||
name: "Test Calendar",
|
||||
color: "#33B679",
|
||||
owner: { firstname: "user1", emails: ["test@test.com"] },
|
||||
visibility: "public",
|
||||
'user1/cal1': {
|
||||
id: 'user1/cal1',
|
||||
name: 'Test Calendar',
|
||||
color: '#33B679',
|
||||
owner: { firstname: 'user1', emails: ['test@test.com'] },
|
||||
visibility: 'public',
|
||||
events: {
|
||||
event1: baseEvent,
|
||||
allDayEvent,
|
||||
},
|
||||
},
|
||||
allDayEvent
|
||||
}
|
||||
}
|
||||
},
|
||||
timeZone: "America/New_York",
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
timeZone: 'America/New_York',
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("does not show timezone offset for all-day events", async () => {
|
||||
it('does not show timezone offset for all-day events', async () => {
|
||||
renderWithProviders(
|
||||
<EventPreviewModal
|
||||
open={true}
|
||||
@@ -199,33 +199,33 @@ describe("EventDisplayPreview - Timezone Display", () => {
|
||||
calId="user1/cal1"
|
||||
/>,
|
||||
baseState
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const title = screen.getByText(/Team Meeting/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
const title = screen.getByText(/Team Meeting/i)
|
||||
expect(title).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should not show UTC offset
|
||||
expect(screen.queryByText(/UTC[+-]\d+/i)).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText(/UTC[+-]\d+/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("displays correct timezone offset for different timezones", async () => {
|
||||
it('displays correct timezone offset for different timezones', async () => {
|
||||
const timezones = [
|
||||
{ tz: "America/New_York", expectedOffset: /UTC[-−][45]/ },
|
||||
{ tz: "Europe/Paris", expectedOffset: /UTC\+[12]/ },
|
||||
{ tz: "Asia/Tokyo", expectedOffset: /UTC\+9/ },
|
||||
{ tz: "Australia/Sydney", expectedOffset: /UTC\+1[01]/ },
|
||||
{ tz: "Asia/Kolkata", expectedOffset: /UTC\+5:30/ },
|
||||
];
|
||||
{ tz: 'America/New_York', expectedOffset: /UTC[-−][45]/ },
|
||||
{ tz: 'Europe/Paris', expectedOffset: /UTC\+[12]/ },
|
||||
{ tz: 'Asia/Tokyo', expectedOffset: /UTC\+9/ },
|
||||
{ tz: 'Australia/Sydney', expectedOffset: /UTC\+1[01]/ },
|
||||
{ tz: 'Asia/Kolkata', expectedOffset: /UTC\+5:30/ }
|
||||
]
|
||||
|
||||
for (const { tz, expectedOffset } of timezones) {
|
||||
const state = {
|
||||
...baseState,
|
||||
settings: {
|
||||
timeZone: tz,
|
||||
},
|
||||
};
|
||||
timeZone: tz
|
||||
}
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<EventPreviewModal
|
||||
@@ -235,12 +235,12 @@ describe("EventDisplayPreview - Timezone Display", () => {
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
state
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const content = document.body.textContent || "";
|
||||
expect(content).toMatch(expectedOffset);
|
||||
});
|
||||
const content = document.body.textContent || ''
|
||||
expect(content).toMatch(expectedOffset)
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,134 +1,134 @@
|
||||
import { TimezoneSelector } from "@/components/Calendar/TimezoneSelector";
|
||||
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import { TimezoneSelector } from '@/components/Calendar/TimezoneSelector'
|
||||
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
describe("TimezoneSelector", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
describe('TimezoneSelector', () => {
|
||||
const mockOnChange = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders with initial timezone value", () => {
|
||||
it('renders with initial timezone value', () => {
|
||||
renderWithProviders(
|
||||
<TimezoneSelector
|
||||
referenceDate={new Date()}
|
||||
value="America/New_York"
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent(/UTC[-−][45]/i); // New York offset
|
||||
});
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(/UTC[-−][45]/i) // New York offset
|
||||
})
|
||||
|
||||
it("opens popover when button is clicked", async () => {
|
||||
it('opens popover when button is clicked', async () => {
|
||||
renderWithProviders(
|
||||
<TimezoneSelector
|
||||
referenceDate={new Date()}
|
||||
value="Europe/Paris"
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
fireEvent.click(button);
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("calls onChange when a new timezone is selected", async () => {
|
||||
it('calls onChange when a new timezone is selected', async () => {
|
||||
renderWithProviders(
|
||||
<TimezoneSelector
|
||||
referenceDate={new Date()}
|
||||
value="Europe/Paris"
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
fireEvent.click(button);
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const autocomplete = screen.getByRole("combobox");
|
||||
fireEvent.change(autocomplete, { target: { value: "Los Angeles" } });
|
||||
const autocomplete = screen.getByRole('combobox')
|
||||
fireEvent.change(autocomplete, { target: { value: 'Los Angeles' } })
|
||||
|
||||
// Find and click the Los Angeles option
|
||||
const option = await screen.findByText(/Los Angeles/i);
|
||||
fireEvent.click(option);
|
||||
const option = await screen.findByText(/Los Angeles/i)
|
||||
fireEvent.click(option)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith("America/Los_Angeles");
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith('America/Los_Angeles')
|
||||
})
|
||||
|
||||
it("closes popover after timezone selection", async () => {
|
||||
it('closes popover after timezone selection', async () => {
|
||||
renderWithProviders(
|
||||
<TimezoneSelector
|
||||
referenceDate={new Date()}
|
||||
value="Europe/Paris"
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
fireEvent.click(button);
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const autocomplete = screen.getByRole("combobox");
|
||||
fireEvent.change(autocomplete, { target: { value: "Tokyo" } });
|
||||
const autocomplete = screen.getByRole('combobox')
|
||||
fireEvent.change(autocomplete, { target: { value: 'Tokyo' } })
|
||||
|
||||
const option = await screen.findByText(/Tokyo/i);
|
||||
fireEvent.click(option);
|
||||
const option = await screen.findByText(/Tokyo/i)
|
||||
fireEvent.click(option)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("displays timezones with half-hour offsets correctly", () => {
|
||||
it('displays timezones with half-hour offsets correctly', () => {
|
||||
renderWithProviders(
|
||||
<TimezoneSelector
|
||||
referenceDate={new Date()}
|
||||
value="Asia/Kolkata"
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveTextContent("UTC+5:30"); // India offset
|
||||
});
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toHaveTextContent('UTC+5:30') // India offset
|
||||
})
|
||||
|
||||
it("shows correct offset for Europe/Paris depending on daylight saving time", () => {
|
||||
it('shows correct offset for Europe/Paris depending on daylight saving time', () => {
|
||||
// Summer date (DST on)
|
||||
const summerDate = new Date("2025-07-15T12:00:00Z");
|
||||
const summerDate = new Date('2025-07-15T12:00:00Z')
|
||||
renderWithProviders(
|
||||
<TimezoneSelector
|
||||
value="Europe/Paris"
|
||||
onChange={mockOnChange}
|
||||
referenceDate={summerDate}
|
||||
/>
|
||||
);
|
||||
let button = screen.getByRole("button");
|
||||
expect(button).toHaveTextContent(/UTC\+2\b/);
|
||||
cleanup();
|
||||
)
|
||||
let button = screen.getByRole('button')
|
||||
expect(button).toHaveTextContent(/UTC\+2\b/)
|
||||
cleanup()
|
||||
// Rerender with a winter date (DST off)
|
||||
const winterDate = new Date("2025-01-15T12:00:00Z");
|
||||
const winterDate = new Date('2025-01-15T12:00:00Z')
|
||||
renderWithProviders(
|
||||
<TimezoneSelector
|
||||
value="Europe/Paris"
|
||||
onChange={mockOnChange}
|
||||
referenceDate={winterDate}
|
||||
/>
|
||||
);
|
||||
button = screen.getByRole("button");
|
||||
expect(button).toHaveTextContent(/UTC\+1\b/);
|
||||
});
|
||||
});
|
||||
)
|
||||
button = screen.getByRole('button')
|
||||
expect(button).toHaveTextContent(/UTC\+1\b/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,152 +1,150 @@
|
||||
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
||||
import { addSharedCalendar } from "@/features/Calendars/CalendarApi";
|
||||
import { fetchOwnerOfResource } from "@/features/Calendars/services/helpers";
|
||||
import { toRejectedError } from "@/utils/errorUtils";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { addCalendarResourceAsync } from '@/features/Calendars/api/addCalendarResourceAsync'
|
||||
import { addSharedCalendar } from '@/features/Calendars/CalendarApi'
|
||||
import { fetchOwnerOfResource } from '@/features/Calendars/services/helpers'
|
||||
import { toRejectedError } from '@/utils/errorUtils'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock("@/features/Calendars/services/helpers");
|
||||
jest.mock("@/utils/errorUtils");
|
||||
jest.mock('@/features/Calendars/CalendarApi')
|
||||
jest.mock('@/features/Calendars/services/helpers')
|
||||
jest.mock('@/utils/errorUtils')
|
||||
|
||||
const mockedAddSharedCalendar = addSharedCalendar as jest.Mock;
|
||||
const mockedFetchOwnerOfResource = fetchOwnerOfResource as jest.Mock;
|
||||
const mockedToRejectedError = toRejectedError as jest.Mock;
|
||||
const mockedAddSharedCalendar = addSharedCalendar as jest.Mock
|
||||
const mockedFetchOwnerOfResource = fetchOwnerOfResource as jest.Mock
|
||||
const mockedToRejectedError = toRejectedError as jest.Mock
|
||||
|
||||
describe("addCalendarResourceAsync thunk", () => {
|
||||
let store: ReturnType<typeof configureStore>;
|
||||
const dispatch = jest.fn();
|
||||
describe('addCalendarResourceAsync thunk', () => {
|
||||
let store: ReturnType<typeof configureStore>
|
||||
const dispatch = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
store = configureStore({
|
||||
reducer: () => ({}),
|
||||
});
|
||||
});
|
||||
reducer: () => ({})
|
||||
})
|
||||
})
|
||||
|
||||
const mockPayload = {
|
||||
userId: "user-123",
|
||||
calId: "cal-123",
|
||||
userId: 'user-123',
|
||||
calId: 'cal-123',
|
||||
cal: {
|
||||
color: {
|
||||
background: "#000000",
|
||||
foreground: "#FFFFFF",
|
||||
background: '#000000',
|
||||
foreground: '#FFFFFF'
|
||||
},
|
||||
cal: {
|
||||
"dav:name": "Resource Room A",
|
||||
"caldav:description": "A meeting room",
|
||||
'dav:name': 'Resource Room A',
|
||||
'caldav:description': 'A meeting room',
|
||||
_links: {
|
||||
self: {
|
||||
href: "/calendars/res-456/cal-123.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
href: '/calendars/res-456/cal-123.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockResolvedResourceData = {
|
||||
_id: "res-456",
|
||||
id: "res-456",
|
||||
name: "Resource Room A",
|
||||
description: "A meeting room",
|
||||
creator: "user-789",
|
||||
_id: 'res-456',
|
||||
id: 'res-456',
|
||||
name: 'Resource Room A',
|
||||
description: 'A meeting room',
|
||||
creator: 'user-789',
|
||||
deleted: false,
|
||||
_rev: "1",
|
||||
};
|
||||
_rev: '1'
|
||||
}
|
||||
|
||||
it("should add shared calendar, fetch resource details, map userData", async () => {
|
||||
it('should add shared calendar, fetch resource details, map userData', async () => {
|
||||
mockedFetchOwnerOfResource.mockResolvedValueOnce({
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: ["creator@example.com"],
|
||||
});
|
||||
mockedAddSharedCalendar.mockResolvedValueOnce({});
|
||||
firstname: 'Creator',
|
||||
lastname: 'User',
|
||||
emails: ['creator@example.com']
|
||||
})
|
||||
mockedAddSharedCalendar.mockResolvedValueOnce({})
|
||||
|
||||
const result = await addCalendarResourceAsync(
|
||||
mockPayload as unknown as Parameters<typeof addCalendarResourceAsync>[0]
|
||||
)(dispatch, store.getState, undefined);
|
||||
)(dispatch, store.getState, undefined)
|
||||
|
||||
expect(mockedAddSharedCalendar).toHaveBeenCalledWith(
|
||||
mockPayload.userId,
|
||||
mockPayload.calId,
|
||||
mockPayload.cal
|
||||
);
|
||||
expect(mockedFetchOwnerOfResource).toHaveBeenCalledWith("res-456");
|
||||
)
|
||||
expect(mockedFetchOwnerOfResource).toHaveBeenCalledWith('res-456')
|
||||
|
||||
expect(result.type).toBe("calendars/addCalendarResource/fulfilled");
|
||||
expect(result.type).toBe('calendars/addCalendarResource/fulfilled')
|
||||
expect(result.payload).toEqual({
|
||||
calId: "res-456/cal-123",
|
||||
color: { background: "#000000", foreground: "#FFFFFF" },
|
||||
desc: "A meeting room",
|
||||
link: "/calendars/user-123/cal-123.json",
|
||||
name: "Resource Room A",
|
||||
calId: 'res-456/cal-123',
|
||||
color: { background: '#000000', foreground: '#FFFFFF' },
|
||||
desc: 'A meeting room',
|
||||
link: '/calendars/user-123/cal-123.json',
|
||||
name: 'Resource Room A',
|
||||
owner: {
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: ["creator@example.com"],
|
||||
resource: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
firstname: 'Creator',
|
||||
lastname: 'User',
|
||||
emails: ['creator@example.com'],
|
||||
resource: true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("should fallback to name if resource details fetch fails", async () => {
|
||||
mockedAddSharedCalendar.mockResolvedValueOnce({});
|
||||
const errorDetails = new Error("Fetch failed");
|
||||
mockedFetchOwnerOfResource.mockRejectedValueOnce(errorDetails);
|
||||
it('should fallback to name if resource details fetch fails', async () => {
|
||||
mockedAddSharedCalendar.mockResolvedValueOnce({})
|
||||
const errorDetails = new Error('Fetch failed')
|
||||
mockedFetchOwnerOfResource.mockRejectedValueOnce(errorDetails)
|
||||
|
||||
// Silence expected console error in tests
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const result = await addCalendarResourceAsync(
|
||||
mockPayload as unknown as Parameters<typeof addCalendarResourceAsync>[0]
|
||||
)(dispatch, store.getState, undefined);
|
||||
)(dispatch, store.getState, undefined)
|
||||
|
||||
expect(mockedAddSharedCalendar).toHaveBeenCalledWith(
|
||||
mockPayload.userId,
|
||||
mockPayload.calId,
|
||||
mockPayload.cal
|
||||
);
|
||||
expect(mockedFetchOwnerOfResource).toHaveBeenCalledWith("res-456");
|
||||
)
|
||||
expect(mockedFetchOwnerOfResource).toHaveBeenCalledWith('res-456')
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
consoleSpy.mockRestore()
|
||||
|
||||
expect(result.type).toBe("calendars/addCalendarResource/fulfilled");
|
||||
expect(result.type).toBe('calendars/addCalendarResource/fulfilled')
|
||||
expect(result.payload).toEqual({
|
||||
calId: "res-456/cal-123",
|
||||
color: { background: "#000000", foreground: "#FFFFFF" },
|
||||
desc: "A meeting room",
|
||||
link: "/calendars/user-123/cal-123.json",
|
||||
name: "Resource Room A",
|
||||
calId: 'res-456/cal-123',
|
||||
color: { background: '#000000', foreground: '#FFFFFF' },
|
||||
desc: 'A meeting room',
|
||||
link: '/calendars/user-123/cal-123.json',
|
||||
name: 'Resource Room A',
|
||||
owner: {
|
||||
firstname: "",
|
||||
lastname: "Resource Room A",
|
||||
firstname: '',
|
||||
lastname: 'Resource Room A',
|
||||
emails: [],
|
||||
resource: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
resource: true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle error if addSharedCalendar fails", async () => {
|
||||
const errorAdd = new Error("Add failed");
|
||||
mockedAddSharedCalendar.mockRejectedValueOnce(errorAdd);
|
||||
const mockRejectedErrorResult = { message: "Add failed" };
|
||||
mockedToRejectedError.mockReturnValueOnce(mockRejectedErrorResult);
|
||||
it('should handle error if addSharedCalendar fails', async () => {
|
||||
const errorAdd = new Error('Add failed')
|
||||
mockedAddSharedCalendar.mockRejectedValueOnce(errorAdd)
|
||||
const mockRejectedErrorResult = { message: 'Add failed' }
|
||||
mockedToRejectedError.mockReturnValueOnce(mockRejectedErrorResult)
|
||||
|
||||
const result = await addCalendarResourceAsync(
|
||||
mockPayload as unknown as Parameters<typeof addCalendarResourceAsync>[0]
|
||||
)(dispatch, store.getState, undefined);
|
||||
)(dispatch, store.getState, undefined)
|
||||
|
||||
expect(mockedAddSharedCalendar).toHaveBeenCalledWith(
|
||||
mockPayload.userId,
|
||||
mockPayload.calId,
|
||||
mockPayload.cal
|
||||
);
|
||||
expect(mockedFetchOwnerOfResource).not.toHaveBeenCalled();
|
||||
)
|
||||
expect(mockedFetchOwnerOfResource).not.toHaveBeenCalled()
|
||||
|
||||
expect(mockedToRejectedError).toHaveBeenCalledWith(errorAdd);
|
||||
expect(mockedToRejectedError).toHaveBeenCalledWith(errorAdd)
|
||||
|
||||
expect(result.type).toBe("calendars/addCalendarResource/rejected");
|
||||
expect(result.payload).toEqual(mockRejectedErrorResult);
|
||||
});
|
||||
});
|
||||
expect(result.type).toBe('calendars/addCalendarResource/rejected')
|
||||
expect(result.payload).toEqual(mockRejectedErrorResult)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,234 +1,234 @@
|
||||
import { getCalendarsListAsync } from "@/features/Calendars/services/getCalendarsListAsync";
|
||||
import { getOpenPaasUser } from "@/features/User/userAPI";
|
||||
import { fetchOwnerData } from "@/features/Calendars/services/helpers";
|
||||
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
||||
import { formatReduxError } from "@/utils/errorUtils";
|
||||
import { normalizeCalendar } from "@/features/Calendars/utils/normalizeCalendar";
|
||||
import { getCalendarsListAsync } from '@/features/Calendars/services/getCalendarsListAsync'
|
||||
import { getOpenPaasUser } from '@/features/User/userAPI'
|
||||
import { fetchOwnerData } from '@/features/Calendars/services/helpers'
|
||||
import { getCalendars } from '@/features/Calendars/CalendarApi'
|
||||
import { formatReduxError } from '@/utils/errorUtils'
|
||||
import { normalizeCalendar } from '@/features/Calendars/utils/normalizeCalendar'
|
||||
|
||||
jest.mock("@/features/User/userAPI");
|
||||
jest.mock("@/features/Calendars/services/helpers");
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock("@/utils/errorUtils");
|
||||
jest.mock("@/features/Calendars/utils/normalizeCalendar");
|
||||
jest.mock("@/utils/getAccessiblePair", () => ({
|
||||
getAccessiblePair: jest.fn().mockReturnValue("#FFF"),
|
||||
}));
|
||||
jest.mock('@/features/User/userAPI')
|
||||
jest.mock('@/features/Calendars/services/helpers')
|
||||
jest.mock('@/features/Calendars/CalendarApi')
|
||||
jest.mock('@/utils/errorUtils')
|
||||
jest.mock('@/features/Calendars/utils/normalizeCalendar')
|
||||
jest.mock('@/utils/getAccessiblePair', () => ({
|
||||
getAccessiblePair: jest.fn().mockReturnValue('#FFF')
|
||||
}))
|
||||
|
||||
jest.mock("@mui/material/styles", () => ({
|
||||
createTheme: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
jest.mock('@mui/material/styles', () => ({
|
||||
createTheme: jest.fn().mockReturnValue({})
|
||||
}))
|
||||
|
||||
const mockedGetOpenPaasUser = getOpenPaasUser as jest.Mock;
|
||||
const mockedFetchOwnerData = fetchOwnerData as jest.Mock;
|
||||
const mockedGetCalendars = getCalendars as jest.Mock;
|
||||
const mockedFormatReduxError = formatReduxError as jest.Mock;
|
||||
const mockedNormalizeCalendar = normalizeCalendar as jest.Mock;
|
||||
const mockedGetOpenPaasUser = getOpenPaasUser as jest.Mock
|
||||
const mockedFetchOwnerData = fetchOwnerData as jest.Mock
|
||||
const mockedGetCalendars = getCalendars as jest.Mock
|
||||
const mockedFormatReduxError = formatReduxError as jest.Mock
|
||||
const mockedNormalizeCalendar = normalizeCalendar as jest.Mock
|
||||
|
||||
describe("getCalendarsListAsync", () => {
|
||||
let dispatch: jest.Mock;
|
||||
let getState: jest.Mock;
|
||||
describe('getCalendarsListAsync', () => {
|
||||
let dispatch: jest.Mock
|
||||
let getState: jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn();
|
||||
getState = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
dispatch = jest.fn()
|
||||
getState = jest.fn()
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockedFormatReduxError.mockImplementation((err) => {
|
||||
if (err?.message) return err.message;
|
||||
if (typeof err === "string") return err;
|
||||
return JSON.stringify(err);
|
||||
});
|
||||
});
|
||||
mockedFormatReduxError.mockImplementation(err => {
|
||||
if (err?.message) return err.message
|
||||
if (typeof err === 'string') return err
|
||||
return JSON.stringify(err)
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle successful execution and merge with existing calendars", async () => {
|
||||
it('should handle successful execution and merge with existing calendars', async () => {
|
||||
getState.mockReturnValue({
|
||||
calendars: {
|
||||
list: {
|
||||
"cal-existing": {
|
||||
id: "cal-existing",
|
||||
color: { light: "red", dark: "#FFF" },
|
||||
events: { "event-1": {} },
|
||||
},
|
||||
},
|
||||
'cal-existing': {
|
||||
id: 'cal-existing',
|
||||
color: { light: 'red', dark: '#FFF' },
|
||||
events: { 'event-1': {} }
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
userData: { openpaasId: "user-123" },
|
||||
},
|
||||
});
|
||||
userData: { openpaasId: 'user-123' }
|
||||
}
|
||||
})
|
||||
|
||||
const mockCalendarsResponse = {
|
||||
_embedded: {
|
||||
"dav:calendar": [{ id: "cal-existing" }, { id: "cal-new" }],
|
||||
},
|
||||
};
|
||||
mockedGetCalendars.mockResolvedValue(mockCalendarsResponse);
|
||||
'dav:calendar': [{ id: 'cal-existing' }, { id: 'cal-new' }]
|
||||
}
|
||||
}
|
||||
mockedGetCalendars.mockResolvedValue(mockCalendarsResponse)
|
||||
|
||||
mockedNormalizeCalendar
|
||||
.mockReturnValueOnce({
|
||||
cal: { "dav:name": "Existing Cal", "apple:color": "blue" },
|
||||
id: "cal-existing",
|
||||
ownerId: "user-123",
|
||||
description: "old cal",
|
||||
cal: { 'dav:name': 'Existing Cal', 'apple:color': 'blue' },
|
||||
id: 'cal-existing',
|
||||
ownerId: 'user-123',
|
||||
description: 'old cal',
|
||||
delegated: false,
|
||||
link: "/link/1",
|
||||
link: '/link/1',
|
||||
visibility: 1,
|
||||
access: 3,
|
||||
access: 3
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
cal: { "dav:name": "New Cal" },
|
||||
id: "cal-new",
|
||||
ownerId: "user-456",
|
||||
description: "new cal",
|
||||
cal: { 'dav:name': 'New Cal' },
|
||||
id: 'cal-new',
|
||||
ownerId: 'user-456',
|
||||
description: 'new cal',
|
||||
delegated: true,
|
||||
link: "/link/2",
|
||||
link: '/link/2',
|
||||
visibility: 2,
|
||||
access: 2,
|
||||
invite: [{ href: "", principal: "", access: 3, inviteStatus: 1 }],
|
||||
});
|
||||
invite: [{ href: '', principal: '', access: 3, inviteStatus: 1 }]
|
||||
})
|
||||
|
||||
mockedFetchOwnerData
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
emails: ["john@example.com"],
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
emails: ['john@example.com']
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Jane",
|
||||
lastname: "Smith",
|
||||
emails: ["jane@example.com"],
|
||||
});
|
||||
firstname: 'Jane',
|
||||
lastname: 'Smith',
|
||||
emails: ['jane@example.com']
|
||||
})
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
const result = await thunk(dispatch, getState, undefined);
|
||||
const thunk = getCalendarsListAsync()
|
||||
const result = await thunk(dispatch, getState, undefined)
|
||||
|
||||
expect(result.type).toBe("calendars/getCalendars/fulfilled");
|
||||
expect(result.type).toBe('calendars/getCalendars/fulfilled')
|
||||
const payload = result.payload as {
|
||||
importedCalendars: any;
|
||||
errors: string;
|
||||
};
|
||||
importedCalendars: any
|
||||
errors: string
|
||||
}
|
||||
|
||||
expect(payload.errors).toBe("");
|
||||
expect(Object.keys(payload.importedCalendars)).toHaveLength(2);
|
||||
expect(payload.errors).toBe('')
|
||||
expect(Object.keys(payload.importedCalendars)).toHaveLength(2)
|
||||
|
||||
expect(payload.importedCalendars["cal-existing"]).toMatchObject({
|
||||
id: "cal-existing",
|
||||
color: { light: "blue", dark: "#FFF" },
|
||||
events: { "event-1": {} },
|
||||
});
|
||||
expect(payload.importedCalendars['cal-existing']).toMatchObject({
|
||||
id: 'cal-existing',
|
||||
color: { light: 'blue', dark: '#FFF' },
|
||||
events: { 'event-1': {} }
|
||||
})
|
||||
|
||||
expect(payload.importedCalendars["cal-new"]).toMatchObject({
|
||||
id: "cal-new",
|
||||
name: "New Cal",
|
||||
owner: { firstname: "Jane", lastname: "Smith" },
|
||||
});
|
||||
expect(payload.importedCalendars['cal-new']).toMatchObject({
|
||||
id: 'cal-new',
|
||||
name: 'New Cal',
|
||||
owner: { firstname: 'Jane', lastname: 'Smith' }
|
||||
})
|
||||
|
||||
expect(mockedGetOpenPaasUser).not.toHaveBeenCalled(); // User ID existed in state
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith("user-123");
|
||||
});
|
||||
expect(mockedGetOpenPaasUser).not.toHaveBeenCalled() // User ID existed in state
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith('user-123')
|
||||
})
|
||||
|
||||
it("should fetch user using getOpenPaasUser if openpaasId is not in state", async () => {
|
||||
getState.mockReturnValue({ calendars: {}, user: {} });
|
||||
mockedGetOpenPaasUser.mockResolvedValue({ id: "fetched-user-123" });
|
||||
mockedGetCalendars.mockResolvedValue({ _embedded: { "dav:calendar": [] } });
|
||||
it('should fetch user using getOpenPaasUser if openpaasId is not in state', async () => {
|
||||
getState.mockReturnValue({ calendars: {}, user: {} })
|
||||
mockedGetOpenPaasUser.mockResolvedValue({ id: 'fetched-user-123' })
|
||||
mockedGetCalendars.mockResolvedValue({ _embedded: { 'dav:calendar': [] } })
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
await thunk(dispatch, getState, undefined);
|
||||
const thunk = getCalendarsListAsync()
|
||||
await thunk(dispatch, getState, undefined)
|
||||
|
||||
expect(mockedGetOpenPaasUser).toHaveBeenCalled();
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith("fetched-user-123");
|
||||
});
|
||||
expect(mockedGetOpenPaasUser).toHaveBeenCalled()
|
||||
expect(mockedGetCalendars).toHaveBeenCalledWith('fetched-user-123')
|
||||
})
|
||||
|
||||
it("should handle error when API call fails", async () => {
|
||||
getState.mockReturnValue({ calendars: {}, user: {} });
|
||||
mockedGetOpenPaasUser.mockResolvedValue({ id: "fetched-user-123" });
|
||||
it('should handle error when API call fails', async () => {
|
||||
getState.mockReturnValue({ calendars: {}, user: {} })
|
||||
mockedGetOpenPaasUser.mockResolvedValue({ id: 'fetched-user-123' })
|
||||
|
||||
mockedGetCalendars.mockRejectedValue({
|
||||
response: { status: 500 },
|
||||
message: "Server Error",
|
||||
});
|
||||
message: 'Server Error'
|
||||
})
|
||||
|
||||
// toRejectedError is imported from a mocked module; provide the expected return
|
||||
const { toRejectedError } = jest.requireMock("@/utils/errorUtils") as {
|
||||
toRejectedError: jest.Mock;
|
||||
};
|
||||
const { toRejectedError } = jest.requireMock('@/utils/errorUtils') as {
|
||||
toRejectedError: jest.Mock
|
||||
}
|
||||
toRejectedError.mockReturnValueOnce({
|
||||
status: 500,
|
||||
message: "Server Error",
|
||||
});
|
||||
message: 'Server Error'
|
||||
})
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
const result = await thunk(dispatch, getState, undefined);
|
||||
const thunk = getCalendarsListAsync()
|
||||
const result = await thunk(dispatch, getState, undefined)
|
||||
|
||||
expect(result.type).toBe("calendars/getCalendars/rejected");
|
||||
expect(result.type).toBe('calendars/getCalendars/rejected')
|
||||
expect(result.payload).toEqual({
|
||||
status: 500,
|
||||
message: "Server Error",
|
||||
});
|
||||
});
|
||||
message: 'Server Error'
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle error when fetching owner data fails", async () => {
|
||||
it('should handle error when fetching owner data fails', async () => {
|
||||
getState.mockReturnValue({
|
||||
calendars: {},
|
||||
user: { userData: { openpaasId: "user-123" } },
|
||||
});
|
||||
user: { userData: { openpaasId: 'user-123' } }
|
||||
})
|
||||
mockedGetCalendars.mockResolvedValue({
|
||||
_embedded: { "dav:calendar": [{ id: "cal-1" }] },
|
||||
});
|
||||
_embedded: { 'dav:calendar': [{ id: 'cal-1' }] }
|
||||
})
|
||||
mockedNormalizeCalendar.mockReturnValue({
|
||||
cal: { "dav:name": "Error Cal" },
|
||||
id: "cal-1",
|
||||
ownerId: "error-123",
|
||||
});
|
||||
cal: { 'dav:name': 'Error Cal' },
|
||||
id: 'cal-1',
|
||||
ownerId: 'error-123'
|
||||
})
|
||||
|
||||
// fetchOwnerData fails
|
||||
mockedFetchOwnerData.mockRejectedValueOnce(new Error("Network Error"));
|
||||
mockedFetchOwnerData.mockRejectedValueOnce(new Error('Network Error'))
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
const result = await thunk(dispatch, getState, undefined);
|
||||
const thunk = getCalendarsListAsync()
|
||||
const result = await thunk(dispatch, getState, undefined)
|
||||
|
||||
const payload = result.payload as any;
|
||||
expect(mockedFetchOwnerData).toHaveBeenCalledWith("error-123");
|
||||
expect(payload.importedCalendars["cal-1"].owner).toEqual({
|
||||
firstname: "",
|
||||
lastname: "Unknown User",
|
||||
emails: [],
|
||||
});
|
||||
const payload = result.payload as any
|
||||
expect(mockedFetchOwnerData).toHaveBeenCalledWith('error-123')
|
||||
expect(payload.importedCalendars['cal-1'].owner).toEqual({
|
||||
firstname: '',
|
||||
lastname: 'Unknown User',
|
||||
emails: []
|
||||
})
|
||||
// Errors array should contain the error
|
||||
expect(payload.errors).toContain("Network Error");
|
||||
});
|
||||
expect(payload.errors).toContain('Network Error')
|
||||
})
|
||||
|
||||
it("should return owner data mapping properly (including resource: true)", async () => {
|
||||
it('should return owner data mapping properly (including resource: true)', async () => {
|
||||
getState.mockReturnValue({
|
||||
calendars: {},
|
||||
user: { userData: { openpaasId: "user-123" } },
|
||||
});
|
||||
user: { userData: { openpaasId: 'user-123' } }
|
||||
})
|
||||
mockedGetCalendars.mockResolvedValue({
|
||||
_embedded: { "dav:calendar": [{ id: "cal-1" }] },
|
||||
});
|
||||
_embedded: { 'dav:calendar': [{ id: 'cal-1' }] }
|
||||
})
|
||||
mockedNormalizeCalendar.mockReturnValue({
|
||||
cal: { "dav:name": "Resource Cal" },
|
||||
id: "cal-1",
|
||||
ownerId: "resource-123",
|
||||
});
|
||||
cal: { 'dav:name': 'Resource Cal' },
|
||||
id: 'cal-1',
|
||||
ownerId: 'resource-123'
|
||||
})
|
||||
|
||||
// fetchOwnerData succeeds and returns a resource config structure
|
||||
mockedFetchOwnerData.mockResolvedValueOnce({
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
firstname: 'Creator',
|
||||
lastname: 'User',
|
||||
emails: [],
|
||||
resource: true,
|
||||
});
|
||||
resource: true
|
||||
})
|
||||
|
||||
const thunk = getCalendarsListAsync();
|
||||
const result = await thunk(dispatch, getState, undefined);
|
||||
const thunk = getCalendarsListAsync()
|
||||
const result = await thunk(dispatch, getState, undefined)
|
||||
|
||||
const payload = result.payload as any;
|
||||
expect(mockedFetchOwnerData).toHaveBeenCalledWith("resource-123");
|
||||
expect(payload.importedCalendars["cal-1"].owner).toEqual({
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
const payload = result.payload as any
|
||||
expect(mockedFetchOwnerData).toHaveBeenCalledWith('resource-123')
|
||||
expect(payload.importedCalendars['cal-1'].owner).toEqual({
|
||||
firstname: 'Creator',
|
||||
lastname: 'User',
|
||||
emails: [],
|
||||
resource: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
resource: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
import { fetchOwnerData } from "@/features/Calendars/services/helpers";
|
||||
import { getResourceDetails, getUserDetails } from "@/features/User/userAPI";
|
||||
import { fetchOwnerData } from '@/features/Calendars/services/helpers'
|
||||
import { getResourceDetails, getUserDetails } from '@/features/User/userAPI'
|
||||
|
||||
jest.mock("@/features/User/userAPI");
|
||||
jest.mock('@/features/User/userAPI')
|
||||
|
||||
const mockedGetUserDetails = getUserDetails as jest.Mock;
|
||||
const mockedGetResourceDetails = getResourceDetails as jest.Mock;
|
||||
const mockedGetUserDetails = getUserDetails as jest.Mock
|
||||
const mockedGetResourceDetails = getResourceDetails as jest.Mock
|
||||
|
||||
describe("helpers", () => {
|
||||
describe('helpers', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("fetchOwnerData", () => {
|
||||
it("should return user details successfully", async () => {
|
||||
describe('fetchOwnerData', () => {
|
||||
it('should return user details successfully', async () => {
|
||||
const mockUser = {
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
emails: ["john@example.com"],
|
||||
};
|
||||
mockedGetUserDetails.mockResolvedValueOnce(mockUser);
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
emails: ['john@example.com']
|
||||
}
|
||||
mockedGetUserDetails.mockResolvedValueOnce(mockUser)
|
||||
|
||||
const result = await fetchOwnerData("user-123");
|
||||
const result = await fetchOwnerData('user-123')
|
||||
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith("user-123");
|
||||
expect(mockedGetResourceDetails).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith('user-123')
|
||||
expect(mockedGetResourceDetails).not.toHaveBeenCalled()
|
||||
expect(result).toEqual(mockUser)
|
||||
})
|
||||
|
||||
it("should fetch resource details and its creator when user is not found", async () => {
|
||||
const mockResource = { creator: "creator-456" };
|
||||
it('should fetch resource details and its creator when user is not found', async () => {
|
||||
const mockResource = { creator: 'creator-456' }
|
||||
const mockCreator = {
|
||||
firstname: "Creator",
|
||||
lastname: "User",
|
||||
emails: ["creator@example.com"],
|
||||
};
|
||||
firstname: 'Creator',
|
||||
lastname: 'User',
|
||||
emails: ['creator@example.com']
|
||||
}
|
||||
|
||||
// Mock getUserDetails to fail with 404 for the initial call
|
||||
mockedGetUserDetails.mockRejectedValueOnce({
|
||||
response: { status: 404 },
|
||||
});
|
||||
response: { status: 404 }
|
||||
})
|
||||
|
||||
// Mock getResourceDetails to succeed and return a creator ID
|
||||
mockedGetResourceDetails.mockResolvedValueOnce(mockResource);
|
||||
mockedGetResourceDetails.mockResolvedValueOnce(mockResource)
|
||||
|
||||
// Mock getUserDetails to succeed when called for the creator
|
||||
mockedGetUserDetails.mockResolvedValueOnce(mockCreator);
|
||||
mockedGetUserDetails.mockResolvedValueOnce(mockCreator)
|
||||
|
||||
const result = await fetchOwnerData("resource-123");
|
||||
const result = await fetchOwnerData('resource-123')
|
||||
|
||||
expect(mockedGetUserDetails).toHaveBeenNthCalledWith(1, "resource-123");
|
||||
expect(mockedGetResourceDetails).toHaveBeenCalledWith("resource-123");
|
||||
expect(mockedGetUserDetails).toHaveBeenNthCalledWith(2, "creator-456");
|
||||
expect(mockedGetUserDetails).toHaveBeenNthCalledWith(1, 'resource-123')
|
||||
expect(mockedGetResourceDetails).toHaveBeenCalledWith('resource-123')
|
||||
expect(mockedGetUserDetails).toHaveBeenNthCalledWith(2, 'creator-456')
|
||||
expect(result).toEqual({
|
||||
...mockCreator,
|
||||
resource: true,
|
||||
administrators: undefined,
|
||||
resourceIcon: undefined,
|
||||
});
|
||||
});
|
||||
resourceIcon: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw error when getUserDetails fails with non-404 error", async () => {
|
||||
const mockError = { response: { status: 500 } };
|
||||
mockedGetUserDetails.mockRejectedValueOnce(mockError);
|
||||
it('should throw error when getUserDetails fails with non-404 error', async () => {
|
||||
const mockError = { response: { status: 500 } }
|
||||
mockedGetUserDetails.mockRejectedValueOnce(mockError)
|
||||
|
||||
await expect(fetchOwnerData("user-123")).rejects.toEqual(mockError);
|
||||
await expect(fetchOwnerData('user-123')).rejects.toEqual(mockError)
|
||||
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith("user-123");
|
||||
expect(mockedGetResourceDetails).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith('user-123')
|
||||
expect(mockedGetResourceDetails).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should throw error when getResourceDetails fails", async () => {
|
||||
const mockError = new Error("Resource not found");
|
||||
it('should throw error when getResourceDetails fails', async () => {
|
||||
const mockError = new Error('Resource not found')
|
||||
|
||||
// Mock getUserDetails to fail with 404 for the initial call
|
||||
mockedGetUserDetails.mockRejectedValueOnce({
|
||||
response: { status: 404 },
|
||||
});
|
||||
response: { status: 404 }
|
||||
})
|
||||
|
||||
// Mock getResourceDetails to fail
|
||||
mockedGetResourceDetails.mockRejectedValueOnce(mockError);
|
||||
mockedGetResourceDetails.mockRejectedValueOnce(mockError)
|
||||
|
||||
await expect(fetchOwnerData("resource-123")).rejects.toEqual(mockError);
|
||||
await expect(fetchOwnerData('resource-123')).rejects.toEqual(mockError)
|
||||
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith("resource-123");
|
||||
expect(mockedGetResourceDetails).toHaveBeenCalledWith("resource-123");
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledTimes(1); // Only called once
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledWith('resource-123')
|
||||
expect(mockedGetResourceDetails).toHaveBeenCalledWith('resource-123')
|
||||
expect(mockedGetUserDetails).toHaveBeenCalledTimes(1) // Only called once
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +1,35 @@
|
||||
import { EventErrorHandler } from "@/components/Error/EventErrorHandler";
|
||||
import { EventErrorHandler } from '@/components/Error/EventErrorHandler'
|
||||
|
||||
describe("EventErrorHandler", () => {
|
||||
it("calls the callback when a new error is reported", () => {
|
||||
const handler = new EventErrorHandler();
|
||||
const callback = jest.fn();
|
||||
describe('EventErrorHandler', () => {
|
||||
it('calls the callback when a new error is reported', () => {
|
||||
const handler = new EventErrorHandler()
|
||||
const callback = jest.fn()
|
||||
|
||||
handler.setErrorCallback(callback);
|
||||
handler.reportError("123", "Something broke");
|
||||
handler.setErrorCallback(callback)
|
||||
handler.reportError('123', 'Something broke')
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(["Something broke"]);
|
||||
});
|
||||
expect(callback).toHaveBeenCalledWith(['Something broke'])
|
||||
})
|
||||
|
||||
it("does not duplicate errors for same eventId", () => {
|
||||
const handler = new EventErrorHandler();
|
||||
const callback = jest.fn();
|
||||
it('does not duplicate errors for same eventId', () => {
|
||||
const handler = new EventErrorHandler()
|
||||
const callback = jest.fn()
|
||||
|
||||
handler.setErrorCallback(callback);
|
||||
handler.reportError("A", "Error 1");
|
||||
handler.reportError("A", "Error 1 again");
|
||||
handler.setErrorCallback(callback)
|
||||
handler.reportError('A', 'Error 1')
|
||||
handler.reportError('A', 'Error 1 again')
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(callback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("clears errors properly", () => {
|
||||
const handler = new EventErrorHandler();
|
||||
const callback = jest.fn();
|
||||
it('clears errors properly', () => {
|
||||
const handler = new EventErrorHandler()
|
||||
const callback = jest.fn()
|
||||
|
||||
handler.setErrorCallback(callback);
|
||||
handler.reportError("A", "Error 1");
|
||||
handler.clearAll();
|
||||
handler.setErrorCallback(callback)
|
||||
handler.reportError('A', 'Error 1')
|
||||
handler.clearAll()
|
||||
|
||||
expect(callback).toHaveBeenLastCalledWith([]);
|
||||
});
|
||||
});
|
||||
expect(callback).toHaveBeenLastCalledWith([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,344 +1,344 @@
|
||||
import SearchBar from "@/components/Menubar/EventSearchBar";
|
||||
import * as searchThunk from "@/features/Search/SearchSlice";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import SearchBar from '@/components/Menubar/EventSearchBar'
|
||||
import * as searchThunk from '@/features/Search/SearchSlice'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
describe("EventSearchBar", () => {
|
||||
const today = new Date();
|
||||
const start = new Date(today);
|
||||
start.setHours(10, 0, 0, 0);
|
||||
const end = new Date(today);
|
||||
end.setHours(11, 0, 0, 0);
|
||||
describe('EventSearchBar', () => {
|
||||
const today = new Date()
|
||||
const start = new Date(today)
|
||||
start.setHours(10, 0, 0, 0)
|
||||
const end = new Date(today)
|
||||
end.setHours(11, 0, 0, 0)
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
tokens: { accessToken: "token" },
|
||||
tokens: { accessToken: 'token' }
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "Calendar personal",
|
||||
id: "user1/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'user1/cal1': {
|
||||
name: 'Calendar personal',
|
||||
id: 'user1/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user1/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: 'user1/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"user2/cal1": {
|
||||
name: "Calendar delegated",
|
||||
'user2/cal1': {
|
||||
name: 'Calendar delegated',
|
||||
delegated: true,
|
||||
id: "user2/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
id: 'user2/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user2/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: 'user2/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"user3/cal1": {
|
||||
name: "Calendar shared",
|
||||
id: "user3/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'user3/cal1': {
|
||||
name: 'Calendar shared',
|
||||
id: 'user3/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user3/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: 'user3/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
it("should render search icon button initially", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('should render search icon button initially', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
expect(searchButton).toBeInTheDocument();
|
||||
});
|
||||
const searchButton = screen.getByRole('button')
|
||||
expect(searchButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should expand search bar when icon is clicked", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('should expand search bar when icon is clicked', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("common.search");
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
});
|
||||
const searchInput = screen.getByPlaceholderText('common.search')
|
||||
expect(searchInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should unexpand search bar when field is unfocused and empty", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('should unexpand search bar when field is unfocused and empty', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("common.search");
|
||||
fireEvent.mouseDown(document.body);
|
||||
expect(searchInput).not.toBeInTheDocument();
|
||||
});
|
||||
const searchInput = screen.getByPlaceholderText('common.search')
|
||||
fireEvent.mouseDown(document.body)
|
||||
expect(searchInput).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should not unexpand search bar when field is unfocused but not empty", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('should not unexpand search bar when field is unfocused but not empty', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("common.search");
|
||||
userEvent.type(searchInput, "test");
|
||||
fireEvent.blur(searchInput);
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
expect(searchInput).toHaveValue("test");
|
||||
});
|
||||
const searchInput = screen.getByPlaceholderText('common.search')
|
||||
userEvent.type(searchInput, 'test')
|
||||
fireEvent.blur(searchInput)
|
||||
expect(searchInput).toBeInTheDocument()
|
||||
expect(searchInput).toHaveValue('test')
|
||||
})
|
||||
|
||||
it("should update search value on input", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('should update search value on input', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("common.search");
|
||||
fireEvent.change(searchInput, { target: { value: "meeting" } });
|
||||
const searchInput = screen.getByPlaceholderText('common.search')
|
||||
fireEvent.change(searchInput, { target: { value: 'meeting' } })
|
||||
|
||||
expect(searchInput).toHaveValue("meeting");
|
||||
});
|
||||
expect(searchInput).toHaveValue('meeting')
|
||||
})
|
||||
|
||||
it("should open filter popover when tune icon is clicked", async () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('should open filter popover when tune icon is clicked', async () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
// Expand search bar
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
// Click tune icon
|
||||
const tuneButton = screen.getByTestId("TuneIcon");
|
||||
if (tuneButton) fireEvent.click(tuneButton);
|
||||
const tuneButton = screen.getByTestId('TuneIcon')
|
||||
if (tuneButton) fireEvent.click(tuneButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("search.searchIn")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText('search.searchIn')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("should clear search value when clear button is clicked", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('should clear search value when clear button is clicked', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("common.search");
|
||||
fireEvent.change(searchInput, { target: { value: "meeting" } });
|
||||
const searchInput = screen.getByPlaceholderText('common.search')
|
||||
fireEvent.change(searchInput, { target: { value: 'meeting' } })
|
||||
|
||||
const clearButton = screen.getByTestId("HighlightOffIcon");
|
||||
fireEvent.click(clearButton);
|
||||
const clearButton = screen.getByTestId('HighlightOffIcon')
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
expect(searchInput).toHaveValue("");
|
||||
});
|
||||
expect(searchInput).toHaveValue('')
|
||||
})
|
||||
|
||||
it("should trigger search on Enter key", async () => {
|
||||
const searchSpy = jest.spyOn(searchThunk, "searchEventsAsync");
|
||||
it('should trigger search on Enter key', async () => {
|
||||
const searchSpy = jest.spyOn(searchThunk, 'searchEventsAsync')
|
||||
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("common.search");
|
||||
fireEvent.change(searchInput, { target: { value: "test" } });
|
||||
fireEvent.keyDown(searchInput, { key: "Enter" });
|
||||
const searchInput = screen.getByPlaceholderText('common.search')
|
||||
fireEvent.change(searchInput, { target: { value: 'test' } })
|
||||
fireEvent.keyDown(searchInput, { key: 'Enter' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchSpy).toHaveBeenCalledWith({
|
||||
filters: {
|
||||
keywords: "",
|
||||
keywords: '',
|
||||
organizers: [],
|
||||
attendees: [],
|
||||
searchIn: ["user1/cal1"],
|
||||
searchIn: ['user1/cal1']
|
||||
},
|
||||
search: "test",
|
||||
});
|
||||
});
|
||||
});
|
||||
it("should not trigger search on Enter key when search is empty", async () => {
|
||||
const searchSpy = jest.spyOn(searchThunk, "searchEventsAsync");
|
||||
search: 'test'
|
||||
})
|
||||
})
|
||||
})
|
||||
it('should not trigger search on Enter key when search is empty', async () => {
|
||||
const searchSpy = jest.spyOn(searchThunk, 'searchEventsAsync')
|
||||
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("common.search");
|
||||
fireEvent.keyDown(searchInput, { key: "Enter" });
|
||||
const searchInput = screen.getByPlaceholderText('common.search')
|
||||
fireEvent.keyDown(searchInput, { key: 'Enter' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(searchSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it("should not trigger search when filter keywords is empty", async () => {
|
||||
const searchSpy = jest.spyOn(searchThunk, "searchEventsAsync");
|
||||
it('should not trigger search when filter keywords is empty', async () => {
|
||||
const searchSpy = jest.spyOn(searchThunk, 'searchEventsAsync')
|
||||
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
const searchButton = screen.getByRole("button");
|
||||
fireEvent.click(searchButton);
|
||||
const searchButton = screen.getByRole('button')
|
||||
fireEvent.click(searchButton)
|
||||
|
||||
// Click tune icon
|
||||
const tuneButton = screen.getByTestId("TuneIcon");
|
||||
if (tuneButton) fireEvent.click(tuneButton);
|
||||
const tuneButton = screen.getByTestId('TuneIcon')
|
||||
if (tuneButton) fireEvent.click(tuneButton)
|
||||
|
||||
fireEvent.click(screen.getByText("common.search"));
|
||||
fireEvent.click(screen.getByText('common.search'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it("keeps search bar expanded when popover opens", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
expect(searchSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
it('keeps search bar expanded when popover opens', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
const tuneBtn = screen
|
||||
.getAllByRole("button")
|
||||
.find((b) => b.querySelector('[data-testid="TuneIcon"]'));
|
||||
.getAllByRole('button')
|
||||
.find(b => b.querySelector('[data-testid="TuneIcon"]'))
|
||||
|
||||
fireEvent.click(tuneBtn!);
|
||||
fireEvent.click(tuneBtn!)
|
||||
|
||||
expect(screen.getByPlaceholderText("common.search")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByPlaceholderText('common.search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does NOT collapse search when clicking inside popover", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('does NOT collapse search when clicking inside popover', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
const tuneBtn = screen
|
||||
.getAllByRole("button")
|
||||
.find((b) => b.querySelector('[data-testid="TuneIcon"]'));
|
||||
.getAllByRole('button')
|
||||
.find(b => b.querySelector('[data-testid="TuneIcon"]'))
|
||||
|
||||
fireEvent.click(tuneBtn!);
|
||||
fireEvent.click(tuneBtn!)
|
||||
|
||||
fireEvent.mouseDown(screen.getByText("search.searchIn"));
|
||||
fireEvent.mouseDown(screen.getByText('search.searchIn'))
|
||||
|
||||
expect(screen.getByPlaceholderText("common.search")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByPlaceholderText('common.search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("clicking Cancel closes popover and search collapses if empty", async () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('clicking Cancel closes popover and search collapses if empty', async () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
const tuneBtn = screen
|
||||
.getAllByRole("button")
|
||||
.find((b) => b.querySelector('[data-testid="TuneIcon"]'));
|
||||
.getAllByRole('button')
|
||||
.find(b => b.querySelector('[data-testid="TuneIcon"]'))
|
||||
|
||||
fireEvent.click(tuneBtn!);
|
||||
fireEvent.click(tuneBtn!)
|
||||
|
||||
fireEvent.click(screen.getByText("common.cancel"));
|
||||
fireEvent.click(screen.getByText('common.cancel'))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText("search.searchIn")).not.toBeInTheDocument()
|
||||
);
|
||||
expect(screen.queryByPlaceholderText("common.search")).toBeNull();
|
||||
});
|
||||
expect(screen.queryByText('search.searchIn')).not.toBeInTheDocument()
|
||||
)
|
||||
expect(screen.queryByPlaceholderText('common.search')).toBeNull()
|
||||
})
|
||||
|
||||
it("does not collapse when selecting a filter value", async () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('does not collapse when selecting a filter value', async () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
const tuneBtn = screen
|
||||
.getAllByRole("button")
|
||||
.find((b) => b.querySelector('[data-testid="TuneIcon"]'));
|
||||
.getAllByRole('button')
|
||||
.find(b => b.querySelector('[data-testid="TuneIcon"]'))
|
||||
|
||||
fireEvent.click(tuneBtn!);
|
||||
fireEvent.click(tuneBtn!)
|
||||
|
||||
fireEvent.mouseDown(screen.getByText("search.searchIn")); // simulate interaction inside menu
|
||||
fireEvent.mouseDown(screen.getByText('search.searchIn')) // simulate interaction inside menu
|
||||
|
||||
expect(screen.getByPlaceholderText("common.search")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByPlaceholderText('common.search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("keeps input value after popover closes if not empty", () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState);
|
||||
it('keeps input value after popover closes if not empty', () => {
|
||||
renderWithProviders(<SearchBar />, preloadedState)
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
userEvent.type(screen.getByPlaceholderText("common.search"), "hello");
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
userEvent.type(screen.getByPlaceholderText('common.search'), 'hello')
|
||||
|
||||
const tuneBtn = screen
|
||||
.getAllByRole("button")
|
||||
.find((b) => b.querySelector('[data-testid="TuneIcon"]'));
|
||||
.getAllByRole('button')
|
||||
.find(b => b.querySelector('[data-testid="TuneIcon"]'))
|
||||
|
||||
fireEvent.click(tuneBtn!);
|
||||
fireEvent.click(document.body);
|
||||
fireEvent.click(tuneBtn!)
|
||||
fireEvent.click(document.body)
|
||||
|
||||
const inputAfter = screen.getByPlaceholderText("common.search");
|
||||
expect(inputAfter).toHaveValue("hello");
|
||||
});
|
||||
});
|
||||
const inputAfter = screen.getByPlaceholderText('common.search')
|
||||
expect(inputAfter).toHaveValue('hello')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,224 +1,224 @@
|
||||
import SearchResultsPage from "@/features/Search/SearchResultsPage";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import SearchResultsPage from '@/features/Search/SearchResultsPage'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
describe("SearchResultsPage", () => {
|
||||
const today = new Date();
|
||||
const start = new Date(today);
|
||||
start.setHours(10, 0, 0, 0);
|
||||
const end = new Date(today);
|
||||
end.setHours(11, 0, 0, 0);
|
||||
describe('SearchResultsPage', () => {
|
||||
const today = new Date()
|
||||
const start = new Date(today)
|
||||
start.setHours(10, 0, 0, 0)
|
||||
const end = new Date(today)
|
||||
end.setHours(11, 0, 0, 0)
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "mockSid",
|
||||
openpaasId: "user1",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'mockSid',
|
||||
openpaasId: 'user1'
|
||||
},
|
||||
tokens: { accessToken: "token" },
|
||||
tokens: { accessToken: 'token' }
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "Calendar personal",
|
||||
id: "user1/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'user1/cal1': {
|
||||
name: 'Calendar personal',
|
||||
id: 'user1/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user1/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: 'user1/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"user2/cal1": {
|
||||
name: "Calendar delegated",
|
||||
'user2/cal1': {
|
||||
name: 'Calendar delegated',
|
||||
delegated: true,
|
||||
id: "user2/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
id: 'user2/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user2/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: 'user2/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"user3/cal1": {
|
||||
name: "Calendar shared",
|
||||
id: "user3/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
owner: { emails: ["alice@example.com"] },
|
||||
'user3/cal1': {
|
||||
name: 'Calendar shared',
|
||||
id: 'user3/cal1',
|
||||
color: { light: '#FF0000', dark: '#000' },
|
||||
owner: { emails: ['alice@example.com'] },
|
||||
events: {
|
||||
event1: {
|
||||
id: "event1",
|
||||
calId: "user3/cal1",
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
id: 'event1',
|
||||
calId: 'user3/cal1',
|
||||
uid: 'event1',
|
||||
title: 'Test Event',
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
partstat: "ACCEPTED",
|
||||
partstat: 'ACCEPTED',
|
||||
organizer: {
|
||||
cn: "Alice",
|
||||
cal_address: "alice@example.com",
|
||||
cn: 'Alice',
|
||||
cal_address: 'alice@example.com'
|
||||
},
|
||||
attendee: [
|
||||
{
|
||||
cn: "Alice",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
cal_address: "alice@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cn: 'Alice',
|
||||
partstat: 'ACCEPTED',
|
||||
rsvp: 'TRUE',
|
||||
role: 'REQ-PARTICIPANT',
|
||||
cutype: 'INDIVIDUAL',
|
||||
cal_address: 'alice@example.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
pending: false
|
||||
}
|
||||
}
|
||||
|
||||
it("should show loading spinner when loading", () => {
|
||||
it('should show loading spinner when loading', () => {
|
||||
renderWithProviders(<SearchResultsPage />, {
|
||||
...preloadedState,
|
||||
searchResult: { loading: true, error: null, hits: 0, results: [] },
|
||||
});
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
});
|
||||
searchResult: { loading: true, error: null, hits: 0, results: [] }
|
||||
})
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show error message when error occurs", () => {
|
||||
it('should show error message when error occurs', () => {
|
||||
renderWithProviders(<SearchResultsPage />, {
|
||||
...preloadedState,
|
||||
searchResult: {
|
||||
loading: false,
|
||||
error: "Network error",
|
||||
error: 'Network error',
|
||||
hits: 0,
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
expect(screen.getByText("Network error")).toBeInTheDocument();
|
||||
});
|
||||
results: []
|
||||
}
|
||||
})
|
||||
expect(screen.getByText('Network error')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show no results message when no hits", () => {
|
||||
it('should show no results message when no hits', () => {
|
||||
renderWithProviders(<SearchResultsPage />, {
|
||||
...preloadedState,
|
||||
searchResult: { loading: false, error: null, hits: null, results: [] },
|
||||
});
|
||||
expect(screen.getByText("search.noResults")).toBeInTheDocument();
|
||||
});
|
||||
searchResult: { loading: false, error: null, hits: null, results: [] }
|
||||
})
|
||||
expect(screen.getByText('search.noResults')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render search results", () => {
|
||||
it('should render search results', () => {
|
||||
const mockResults = [
|
||||
{
|
||||
data: {
|
||||
uid: "1",
|
||||
summary: "Team Meeting",
|
||||
start: "2025-06-26T15:00:00Z",
|
||||
organizer: { cn: "John Doe", email: "john@example.com" },
|
||||
allDay: false,
|
||||
},
|
||||
uid: '1',
|
||||
summary: 'Team Meeting',
|
||||
start: '2025-06-26T15:00:00Z',
|
||||
organizer: { cn: 'John Doe', email: 'john@example.com' },
|
||||
allDay: false
|
||||
}
|
||||
},
|
||||
{
|
||||
data: {
|
||||
uid: "2",
|
||||
summary: "Project Review",
|
||||
start: "2025-06-27T10:00:00Z",
|
||||
organizer: { cn: "Jane Smith", email: "jane@example.com" },
|
||||
allDay: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
uid: '2',
|
||||
summary: 'Project Review',
|
||||
start: '2025-06-27T10:00:00Z',
|
||||
organizer: { cn: 'Jane Smith', email: 'jane@example.com' },
|
||||
allDay: false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
renderWithProviders(<SearchResultsPage />, {
|
||||
...preloadedState,
|
||||
searchResult: { results: mockResults, hits: 2 },
|
||||
});
|
||||
expect(screen.getByText("search.resultsTitle")).toBeInTheDocument();
|
||||
expect(screen.getByText("Team Meeting")).toBeInTheDocument();
|
||||
expect(screen.getByText("Project Review")).toBeInTheDocument();
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
expect(screen.getByText("Jane Smith")).toBeInTheDocument();
|
||||
});
|
||||
searchResult: { results: mockResults, hits: 2 }
|
||||
})
|
||||
expect(screen.getByText('search.resultsTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('Team Meeting')).toBeInTheDocument()
|
||||
expect(screen.getByText('Project Review')).toBeInTheDocument()
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
||||
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle events without organizer", () => {
|
||||
it('should handle events without organizer', () => {
|
||||
const mockResults = [
|
||||
{
|
||||
data: {
|
||||
uid: "1",
|
||||
summary: "Untitled Event",
|
||||
start: "2025-06-26T15:00:00Z",
|
||||
allDay: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
uid: '1',
|
||||
summary: 'Untitled Event',
|
||||
start: '2025-06-26T15:00:00Z',
|
||||
allDay: false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
renderWithProviders(<SearchResultsPage />, {
|
||||
...preloadedState,
|
||||
searchResult: { results: mockResults, hits: 1 },
|
||||
});
|
||||
searchResult: { results: mockResults, hits: 1 }
|
||||
})
|
||||
|
||||
expect(screen.getByText("Untitled Event")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Untitled Event')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should format all-day events correctly", () => {
|
||||
it('should format all-day events correctly', () => {
|
||||
const mockResults = [
|
||||
{
|
||||
data: {
|
||||
uid: "1",
|
||||
summary: "All Day Event",
|
||||
start: "2025-06-26T00:00:00Z",
|
||||
organizer: { cn: "Organizer" },
|
||||
allDay: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
uid: '1',
|
||||
summary: 'All Day Event',
|
||||
start: '2025-06-26T00:00:00Z',
|
||||
organizer: { cn: 'Organizer' },
|
||||
allDay: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
renderWithProviders(<SearchResultsPage />, {
|
||||
...preloadedState,
|
||||
searchResult: { results: mockResults, hits: 1 },
|
||||
});
|
||||
searchResult: { results: mockResults, hits: 1 }
|
||||
})
|
||||
|
||||
expect(screen.getByText("All Day Event")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByText('All Day Event')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,126 +1,126 @@
|
||||
import * as EventApi from "@/features/Events/EventApi";
|
||||
import * as EventApi from '@/features/Events/EventApi'
|
||||
import searchResultReducer, {
|
||||
searchEventsAsync,
|
||||
setHits,
|
||||
setResults,
|
||||
} from "@/features/Search/SearchSlice";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
setResults
|
||||
} from '@/features/Search/SearchSlice'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
|
||||
jest.mock("@/features/Events/EventApi");
|
||||
jest.mock('@/features/Events/EventApi')
|
||||
|
||||
describe("SearchSlice", () => {
|
||||
let store: any;
|
||||
describe('SearchSlice', () => {
|
||||
let store: any
|
||||
|
||||
beforeEach(() => {
|
||||
store = configureStore({
|
||||
reducer: {
|
||||
searchResult: searchResultReducer,
|
||||
},
|
||||
});
|
||||
});
|
||||
searchResult: searchResultReducer
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("initial state", () => {
|
||||
it("should have correct initial state", () => {
|
||||
const state = store.getState().searchResult;
|
||||
describe('initial state', () => {
|
||||
it('should have correct initial state', () => {
|
||||
const state = store.getState().searchResult
|
||||
expect(state).toEqual({
|
||||
results: [],
|
||||
hits: 0,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
loading: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("reducers", () => {
|
||||
it("should handle setResults", () => {
|
||||
describe('reducers', () => {
|
||||
it('should handle setResults', () => {
|
||||
const mockResults = [
|
||||
{ uid: "1", summary: "Event 1" },
|
||||
{ uid: "2", summary: "Event 2" },
|
||||
];
|
||||
store.dispatch(setResults(mockResults as any));
|
||||
expect(store.getState().searchResult.results).toEqual(mockResults);
|
||||
});
|
||||
{ uid: '1', summary: 'Event 1' },
|
||||
{ uid: '2', summary: 'Event 2' }
|
||||
]
|
||||
store.dispatch(setResults(mockResults as any))
|
||||
expect(store.getState().searchResult.results).toEqual(mockResults)
|
||||
})
|
||||
|
||||
it("should handle setHits", () => {
|
||||
store.dispatch(setHits(42));
|
||||
expect(store.getState().searchResult.hits).toBe(42);
|
||||
});
|
||||
});
|
||||
it('should handle setHits', () => {
|
||||
store.dispatch(setHits(42))
|
||||
expect(store.getState().searchResult.hits).toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
describe("searchEventsAsync", () => {
|
||||
describe('searchEventsAsync', () => {
|
||||
const mockFilters = {
|
||||
searchIn: ["user1/calendar1"],
|
||||
keywords: "meeting",
|
||||
organizers: ["user@example.com"],
|
||||
attendees: ["attendee@example.com"],
|
||||
};
|
||||
searchIn: ['user1/calendar1'],
|
||||
keywords: 'meeting',
|
||||
organizers: ['user@example.com'],
|
||||
attendees: ['attendee@example.com']
|
||||
}
|
||||
|
||||
it("should handle successful search", async () => {
|
||||
it('should handle successful search', async () => {
|
||||
const mockResponse = {
|
||||
_total_hits: 5,
|
||||
_embedded: {
|
||||
events: [
|
||||
{ data: { uid: "1", summary: "Test Event" } },
|
||||
{ data: { uid: "2", summary: "Another Event" } },
|
||||
],
|
||||
},
|
||||
};
|
||||
{ data: { uid: '1', summary: 'Test Event' } },
|
||||
{ data: { uid: '2', summary: 'Another Event' } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
(EventApi.searchEvent as jest.Mock).mockResolvedValue(mockResponse);
|
||||
;(EventApi.searchEvent as jest.Mock).mockResolvedValue(mockResponse)
|
||||
|
||||
await store.dispatch(
|
||||
searchEventsAsync({ search: "test", filters: mockFilters })
|
||||
);
|
||||
searchEventsAsync({ search: 'test', filters: mockFilters })
|
||||
)
|
||||
|
||||
const state = store.getState().searchResult;
|
||||
expect(state.loading).toBe(false);
|
||||
expect(state.hits).toBe(5);
|
||||
expect(state.results).toEqual(mockResponse._embedded.events);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
const state = store.getState().searchResult
|
||||
expect(state.loading).toBe(false)
|
||||
expect(state.hits).toBe(5)
|
||||
expect(state.results).toEqual(mockResponse._embedded.events)
|
||||
expect(state.error).toBeNull()
|
||||
})
|
||||
|
||||
it("should handle search with no results", async () => {
|
||||
it('should handle search with no results', async () => {
|
||||
const mockResponse = {
|
||||
_total_hits: 0,
|
||||
_embedded: { events: [] },
|
||||
};
|
||||
_embedded: { events: [] }
|
||||
}
|
||||
|
||||
(EventApi.searchEvent as jest.Mock).mockResolvedValue(mockResponse);
|
||||
;(EventApi.searchEvent as jest.Mock).mockResolvedValue(mockResponse)
|
||||
|
||||
await store.dispatch(
|
||||
searchEventsAsync({ search: "nonexistent", filters: mockFilters })
|
||||
);
|
||||
searchEventsAsync({ search: 'nonexistent', filters: mockFilters })
|
||||
)
|
||||
|
||||
const state = store.getState().searchResult;
|
||||
expect(state.hits).toBe(0);
|
||||
expect(state.results).toEqual([]);
|
||||
});
|
||||
const state = store.getState().searchResult
|
||||
expect(state.hits).toBe(0)
|
||||
expect(state.results).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle search error", async () => {
|
||||
const mockError = new Error("Network error");
|
||||
(EventApi.searchEvent as jest.Mock).mockRejectedValue(mockError);
|
||||
it('should handle search error', async () => {
|
||||
const mockError = new Error('Network error')
|
||||
;(EventApi.searchEvent as jest.Mock).mockRejectedValue(mockError)
|
||||
|
||||
await store.dispatch(
|
||||
searchEventsAsync({ search: "test", filters: mockFilters })
|
||||
);
|
||||
searchEventsAsync({ search: 'test', filters: mockFilters })
|
||||
)
|
||||
|
||||
const state = store.getState().searchResult;
|
||||
expect(state.loading).toBe(false);
|
||||
expect(state.error).toBeTruthy();
|
||||
expect(state.results).toEqual([]);
|
||||
});
|
||||
const state = store.getState().searchResult
|
||||
expect(state.loading).toBe(false)
|
||||
expect(state.error).toBeTruthy()
|
||||
expect(state.results).toEqual([])
|
||||
})
|
||||
|
||||
it("should set loading state during search", async () => {
|
||||
(EventApi.searchEvent as jest.Mock).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(resolve, 100))
|
||||
);
|
||||
it('should set loading state during search', async () => {
|
||||
;(EventApi.searchEvent as jest.Mock).mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(resolve, 100))
|
||||
)
|
||||
|
||||
const promise = store.dispatch(
|
||||
searchEventsAsync({ search: "test", filters: mockFilters })
|
||||
);
|
||||
searchEventsAsync({ search: 'test', filters: mockFilters })
|
||||
)
|
||||
|
||||
expect(store.getState().searchResult.loading).toBe(true);
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(store.getState().searchResult.loading).toBe(true)
|
||||
await promise
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,482 +1,480 @@
|
||||
import SettingsPage from "@/features/Settings/SettingsPage";
|
||||
import SettingsPage from '@/features/Settings/SettingsPage'
|
||||
import settingsReducer, {
|
||||
setIsBrowserDefaultTimeZone,
|
||||
setTimeZone,
|
||||
} from "@/features/Settings/SettingsSlice";
|
||||
setTimeZone
|
||||
} from '@/features/Settings/SettingsSlice'
|
||||
import userReducer, {
|
||||
getOpenPaasUserDataAsync,
|
||||
setTimezone as setUserTimeZone,
|
||||
} from "@/features/User/userSlice";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { browserDefaultTimeZone } from "@/utils/timezone";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
setTimezone as setUserTimeZone
|
||||
} from '@/features/User/userSlice'
|
||||
import { api } from '@/utils/apiUtils'
|
||||
import { browserDefaultTimeZone } from '@/utils/timezone'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/utils/apiUtils");
|
||||
jest.mock('@/utils/apiUtils')
|
||||
|
||||
describe("Timezone synchronization after getOpenPaasUserDataAsync", () => {
|
||||
let apiGetSpy: jest.SpyInstance;
|
||||
describe('Timezone synchronization after getOpenPaasUserDataAsync', () => {
|
||||
let apiGetSpy: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
apiGetSpy = jest.spyOn(api, "get");
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
localStorage.clear()
|
||||
apiGetSpy = jest.spyOn(api, 'get')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
apiGetSpy.mockRestore();
|
||||
});
|
||||
apiGetSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should sync timezone to both user and settings state after fetching user data", async () => {
|
||||
it('should sync timezone to both user and settings state after fetching user data', async () => {
|
||||
const mockUserData = {
|
||||
id: "667037022b752d0026472254",
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
preferredEmail: ["test@test.com"],
|
||||
id: '667037022b752d0026472254',
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
preferredEmail: ['test@test.com'],
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: [
|
||||
{ name: "language", value: "fr" },
|
||||
{ name: "datetime", value: { timeZone: "Europe/Paris" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
{ name: 'language', value: 'fr' },
|
||||
{ name: 'datetime', value: { timeZone: 'Europe/Paris' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Return a fake fetch-like response
|
||||
apiGetSpy.mockResolvedValue({
|
||||
json: async () => mockUserData,
|
||||
});
|
||||
json: async () => mockUserData
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: { user: userReducer, settings: settingsReducer },
|
||||
preloadedState: {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
name: 'John',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
|
||||
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: { language: "en", timeZone: "UTC", view: "calendar" },
|
||||
},
|
||||
});
|
||||
settings: { language: 'en', timeZone: 'UTC', view: 'calendar' }
|
||||
}
|
||||
})
|
||||
|
||||
const result = await store.dispatch(getOpenPaasUserDataAsync());
|
||||
const result = await store.dispatch(getOpenPaasUserDataAsync())
|
||||
|
||||
expect(result.type).toBe("user/getOpenPaasUserData/fulfilled");
|
||||
expect(result.type).toBe('user/getOpenPaasUserData/fulfilled')
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe("Europe/Paris");
|
||||
expect(state.settings.timeZone).toBe("Europe/Paris");
|
||||
expect(localStorage.getItem("timeZone")).toBe("Europe/Paris");
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe('Europe/Paris')
|
||||
expect(state.settings.timeZone).toBe('Europe/Paris')
|
||||
expect(localStorage.getItem('timeZone')).toBe('Europe/Paris')
|
||||
})
|
||||
|
||||
it("should keep browser timezone if API response has no timezone configuration", async () => {
|
||||
it('should keep browser timezone if API response has no timezone configuration', async () => {
|
||||
const mockUserData = {
|
||||
id: "667037022b752d0026472254",
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
preferredEmail: "[test@test.com](mailto:test@test.com)",
|
||||
id: '667037022b752d0026472254',
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
preferredEmail: '[test@test.com](mailto:test@test.com)',
|
||||
configurations: {
|
||||
modules: [
|
||||
{ name: "core", configurations: [{ name: "language", value: "en" }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
{ name: 'core', configurations: [{ name: 'language', value: 'en' }] }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
apiGetSpy.mockResolvedValue({
|
||||
json: async () => mockUserData,
|
||||
});
|
||||
json: async () => mockUserData
|
||||
})
|
||||
|
||||
const browserTimezone = browserDefaultTimeZone ?? "UTC";
|
||||
const browserTimezone = browserDefaultTimeZone ?? 'UTC'
|
||||
|
||||
const store = configureStore({
|
||||
reducer: { user: userReducer, settings: settingsReducer },
|
||||
preloadedState: {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
name: 'John',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: {
|
||||
language: "en",
|
||||
datetime: { timeZone: browserTimezone },
|
||||
language: 'en',
|
||||
datetime: { timeZone: browserTimezone }
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
language: 'en',
|
||||
timeZone: browserTimezone,
|
||||
view: "calendar",
|
||||
},
|
||||
},
|
||||
});
|
||||
view: 'calendar'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = await store.dispatch(getOpenPaasUserDataAsync());
|
||||
const result = await store.dispatch(getOpenPaasUserDataAsync())
|
||||
|
||||
expect(result.type).toBe("user/getOpenPaasUserData/fulfilled");
|
||||
expect(result.type).toBe('user/getOpenPaasUserData/fulfilled')
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(null);
|
||||
expect(state.settings.timeZone).toBe(browserTimezone);
|
||||
});
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(null)
|
||||
expect(state.settings.timeZone).toBe(browserTimezone)
|
||||
})
|
||||
})
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
let store: Record<string, string> = {}
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value;
|
||||
store[key] = value
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key];
|
||||
delete store[key]
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
store = {}
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: localStorageMock,
|
||||
});
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
})
|
||||
|
||||
describe("Timezone Logic - Backend to Frontend Flow", () => {
|
||||
let store: any;
|
||||
describe('Timezone Logic - Backend to Frontend Flow', () => {
|
||||
let store: any
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
localStorageMock.clear()
|
||||
store = configureStore({
|
||||
reducer: {
|
||||
settings: settingsReducer,
|
||||
user: userReducer,
|
||||
user: userReducer
|
||||
},
|
||||
preloadedState: {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
name: 'John',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
|
||||
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: { language: "en", timeZone: "UTC", view: "calendar" },
|
||||
},
|
||||
});
|
||||
});
|
||||
settings: { language: 'en', timeZone: 'UTC', view: 'calendar' }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Backend Response Handling", () => {
|
||||
test("Backend returns NULL => Should use browser default timezone", async () => {
|
||||
describe('Backend Response Handling', () => {
|
||||
test('Backend returns NULL => Should use browser default timezone', async () => {
|
||||
const backendResponse = {
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
id: "123",
|
||||
preferredEmail: "john@example.com",
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
id: '123',
|
||||
preferredEmail: 'john@example.com',
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: [
|
||||
{
|
||||
name: "datetime",
|
||||
name: 'datetime',
|
||||
value: {
|
||||
timeZone: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
timeZone: null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
|
||||
);
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
|
||||
)
|
||||
|
||||
const settingsState = store.getState().settings;
|
||||
const userState = store.getState().user;
|
||||
const settingsState = store.getState().settings
|
||||
const userState = store.getState().user
|
||||
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(true);
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(null);
|
||||
expect(localStorage.getItem("timeZone")).toBe(browserDefaultTimeZone);
|
||||
});
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone)
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(true)
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(null)
|
||||
expect(localStorage.getItem('timeZone')).toBe(browserDefaultTimeZone)
|
||||
})
|
||||
|
||||
test("Backend returns SPECIFIC VALUE => Should use that value", async () => {
|
||||
const specificTimezone = "America/New_York";
|
||||
test('Backend returns SPECIFIC VALUE => Should use that value', async () => {
|
||||
const specificTimezone = 'America/New_York'
|
||||
const backendResponse = {
|
||||
firstname: "Jane",
|
||||
lastname: "Smith",
|
||||
id: "456",
|
||||
preferredEmail: "jane@example.com",
|
||||
firstname: 'Jane',
|
||||
lastname: 'Smith',
|
||||
id: '456',
|
||||
preferredEmail: 'jane@example.com',
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: [
|
||||
{
|
||||
name: "datetime",
|
||||
name: 'datetime',
|
||||
value: {
|
||||
timeZone: specificTimezone,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
timeZone: specificTimezone
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
|
||||
);
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
|
||||
)
|
||||
|
||||
const settingsState = store.getState().settings;
|
||||
const userState = store.getState().user;
|
||||
const settingsState = store.getState().settings
|
||||
const userState = store.getState().user
|
||||
|
||||
expect(settingsState.timeZone).toBe(specificTimezone);
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(false);
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(specificTimezone);
|
||||
expect(localStorage.getItem("timeZone")).toBe(specificTimezone);
|
||||
});
|
||||
expect(settingsState.timeZone).toBe(specificTimezone)
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(false)
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(specificTimezone)
|
||||
expect(localStorage.getItem('timeZone')).toBe(specificTimezone)
|
||||
})
|
||||
|
||||
test("Backend returns NO datetime config => Should use browser default", async () => {
|
||||
test('Backend returns NO datetime config => Should use browser default', async () => {
|
||||
const backendResponse = {
|
||||
firstname: "Bob",
|
||||
lastname: "Johnson",
|
||||
id: "789",
|
||||
preferredEmail: "bob@example.com",
|
||||
firstname: 'Bob',
|
||||
lastname: 'Johnson',
|
||||
id: '789',
|
||||
preferredEmail: 'bob@example.com',
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: [
|
||||
// No datetime config
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
|
||||
);
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
|
||||
)
|
||||
|
||||
const settingsState = store.getState().settings;
|
||||
const userState = store.getState().user;
|
||||
const settingsState = store.getState().settings
|
||||
const userState = store.getState().user
|
||||
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(true);
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(null);
|
||||
expect(localStorage.getItem("timeZone")).toBe(browserDefaultTimeZone);
|
||||
});
|
||||
});
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone)
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(true)
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(null)
|
||||
expect(localStorage.getItem('timeZone')).toBe(browserDefaultTimeZone)
|
||||
})
|
||||
})
|
||||
|
||||
test("Settings state ALWAYS has a concrete value (never null)", () => {
|
||||
test('Settings state ALWAYS has a concrete value (never null)', () => {
|
||||
// Test 1: Browser default scenario
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(true));
|
||||
let settingsState = store.getState().settings;
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(true))
|
||||
let settingsState = store.getState().settings
|
||||
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(settingsState.timeZone).not.toBe(null);
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone)
|
||||
expect(settingsState.timeZone).not.toBe(null)
|
||||
|
||||
// Test 2: Specific timezone scenario
|
||||
store.dispatch(setTimeZone("Europe/Paris"));
|
||||
settingsState = store.getState().settings;
|
||||
store.dispatch(setTimeZone('Europe/Paris'))
|
||||
settingsState = store.getState().settings
|
||||
|
||||
expect(settingsState.timeZone).toBe("Europe/Paris");
|
||||
expect(settingsState.timeZone).not.toBe(null);
|
||||
});
|
||||
expect(settingsState.timeZone).toBe('Europe/Paris')
|
||||
expect(settingsState.timeZone).not.toBe(null)
|
||||
})
|
||||
|
||||
describe("User Actions - Changing Timezone", () => {
|
||||
describe('User Actions - Changing Timezone', () => {
|
||||
beforeEach(() => {
|
||||
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||
});
|
||||
test("User enables browser default => Settings gets browser TZ, User gets null", async () => {
|
||||
;(api.patch as jest.Mock).mockResolvedValue({ status: 204 })
|
||||
})
|
||||
test('User enables browser default => Settings gets browser TZ, User gets null', async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, {
|
||||
user: {
|
||||
userData: { sub: "test" },
|
||||
userData: { sub: 'test' },
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: {
|
||||
language: "en",
|
||||
datetime: { timeZone: "America/Los_Angeles" },
|
||||
language: 'en',
|
||||
datetime: { timeZone: 'America/Los_Angeles' }
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
timeZone: "America/Los_Angeles",
|
||||
language: 'en',
|
||||
timeZone: 'America/Los_Angeles',
|
||||
isBrowserDefaultTimeZone: false,
|
||||
view: "settings",
|
||||
},
|
||||
});
|
||||
view: 'settings'
|
||||
}
|
||||
})
|
||||
|
||||
const browserDefaultSwitch = screen.getAllByLabelText(
|
||||
"settings.timeZoneBrowserDefault"
|
||||
)[0];
|
||||
expect(browserDefaultSwitch).toBeInTheDocument();
|
||||
'settings.timeZoneBrowserDefault'
|
||||
)[0]
|
||||
expect(browserDefaultSwitch).toBeInTheDocument()
|
||||
|
||||
// Enable browser default
|
||||
fireEvent.click(browserDefaultSwitch);
|
||||
fireEvent.click(browserDefaultSwitch)
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.isBrowserDefaultTimeZone).toBe(true);
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.settings.isBrowserDefaultTimeZone).toBe(true)
|
||||
})
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.timeZone).toBe(browserDefaultTimeZone);
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.settings.timeZone).toBe(browserDefaultTimeZone)
|
||||
})
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(null);
|
||||
});
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
test("User selects specific timezone => Both states get the value", async () => {
|
||||
test('User selects specific timezone => Both states get the value', async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, {
|
||||
user: {
|
||||
userData: { sub: "test" },
|
||||
userData: { sub: 'test' },
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: { language: "en", datetime: { timeZone: null } },
|
||||
coreConfig: { language: 'en', datetime: { timeZone: null } },
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
language: 'en',
|
||||
timeZone: browserDefaultTimeZone,
|
||||
isBrowserDefaultTimeZone: true,
|
||||
view: "settings",
|
||||
},
|
||||
});
|
||||
view: 'settings'
|
||||
}
|
||||
})
|
||||
|
||||
const browserDefaultSwitch = screen.getAllByLabelText(
|
||||
"settings.timeZoneBrowserDefault"
|
||||
)[0];
|
||||
'settings.timeZoneBrowserDefault'
|
||||
)[0]
|
||||
|
||||
// Disable browser default so manual selector appears
|
||||
fireEvent.click(browserDefaultSwitch);
|
||||
fireEvent.click(browserDefaultSwitch)
|
||||
|
||||
// Now timezone combobox is visible
|
||||
const timezoneInput = screen.getAllByRole("combobox")[1];
|
||||
const timezoneInput = screen.getAllByRole('combobox')[1]
|
||||
|
||||
// Type to filter options
|
||||
fireEvent.change(timezoneInput, {
|
||||
target: { value: "Australia/Sydney" },
|
||||
});
|
||||
target: { value: 'Australia/Sydney' }
|
||||
})
|
||||
|
||||
// Select from autocomplete dropdown
|
||||
const option = await screen.findByText(/Australia\/Sydney/i);
|
||||
fireEvent.click(option);
|
||||
const option = await screen.findByText(/Australia\/Sydney/i)
|
||||
fireEvent.click(option)
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.isBrowserDefaultTimeZone).toBe(false);
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.settings.isBrowserDefaultTimeZone).toBe(false)
|
||||
})
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.timeZone).toBe("Australia/Sydney");
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.settings.timeZone).toBe('Australia/Sydney')
|
||||
})
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(
|
||||
"Australia/Sydney"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe('Australia/Sydney')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("LocalStorage Persistence", () => {
|
||||
test("LocalStorage always stores concrete values (browser TZ or specific TZ)", () => {
|
||||
describe('LocalStorage Persistence', () => {
|
||||
test('LocalStorage always stores concrete values (browser TZ or specific TZ)', () => {
|
||||
// Scenario 1: Browser default
|
||||
store.dispatch(setTimeZone(browserDefaultTimeZone));
|
||||
expect(localStorage.getItem("timeZone")).toBe(browserDefaultTimeZone);
|
||||
expect(localStorage.getItem("timeZone")).not.toBe("null");
|
||||
store.dispatch(setTimeZone(browserDefaultTimeZone))
|
||||
expect(localStorage.getItem('timeZone')).toBe(browserDefaultTimeZone)
|
||||
expect(localStorage.getItem('timeZone')).not.toBe('null')
|
||||
|
||||
// Scenario 2: Specific timezone
|
||||
store.dispatch(setTimeZone("Africa/Cairo"));
|
||||
expect(localStorage.getItem("timeZone")).toBe("Africa/Cairo");
|
||||
});
|
||||
});
|
||||
store.dispatch(setTimeZone('Africa/Cairo'))
|
||||
expect(localStorage.getItem('timeZone')).toBe('Africa/Cairo')
|
||||
})
|
||||
})
|
||||
|
||||
test("Full user journey: Backend null -> User changes -> Backend saves", async () => {
|
||||
test('Full user journey: Backend null -> User changes -> Backend saves', async () => {
|
||||
// Step 1: Backend returns null
|
||||
const backendResponseNull = {
|
||||
firstname: "Test",
|
||||
lastname: "User",
|
||||
id: "999",
|
||||
preferredEmail: "test@example.com",
|
||||
firstname: 'Test',
|
||||
lastname: 'User',
|
||||
id: '999',
|
||||
preferredEmail: 'test@example.com',
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: [
|
||||
{
|
||||
name: "datetime",
|
||||
value: { timeZone: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
name: 'datetime',
|
||||
value: { timeZone: null }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponseNull, "", undefined)
|
||||
);
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponseNull, '', undefined)
|
||||
)
|
||||
|
||||
// Verify initial state
|
||||
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null);
|
||||
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone)
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null)
|
||||
|
||||
// Step 2: User changes to specific timezone
|
||||
const userSelectedTZ = "Europe/London";
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(false));
|
||||
store.dispatch(setTimeZone(userSelectedTZ));
|
||||
store.dispatch(setUserTimeZone(userSelectedTZ));
|
||||
const userSelectedTZ = 'Europe/London'
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(false))
|
||||
store.dispatch(setTimeZone(userSelectedTZ))
|
||||
store.dispatch(setUserTimeZone(userSelectedTZ))
|
||||
|
||||
expect(store.getState().settings.timeZone).toBe(userSelectedTZ);
|
||||
expect(store.getState().settings.timeZone).toBe(userSelectedTZ)
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(
|
||||
userSelectedTZ
|
||||
);
|
||||
)
|
||||
|
||||
// Step 3: User switches back to browser default
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(true));
|
||||
store.dispatch(setUserTimeZone(null));
|
||||
store.dispatch(setTimeZone(browserDefaultTimeZone));
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(true))
|
||||
store.dispatch(setUserTimeZone(null))
|
||||
store.dispatch(setTimeZone(browserDefaultTimeZone))
|
||||
|
||||
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null);
|
||||
});
|
||||
});
|
||||
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone)
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,467 +1,463 @@
|
||||
import SettingsPage from "@/features/Settings/SettingsPage";
|
||||
import settingsReducer from "@/features/Settings/SettingsSlice";
|
||||
import SettingsPage from '@/features/Settings/SettingsPage'
|
||||
import settingsReducer from '@/features/Settings/SettingsSlice'
|
||||
import userReducer, {
|
||||
getOpenPaasUserDataAsync,
|
||||
} from "@/features/User/userSlice";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
getOpenPaasUserDataAsync
|
||||
} from '@/features/User/userSlice'
|
||||
import { api } from '@/utils/apiUtils'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import '@testing-library/jest-dom'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
jest.mock("@/utils/apiUtils");
|
||||
jest.mock('@/utils/apiUtils')
|
||||
|
||||
const basePreloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
name: 'John',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: {
|
||||
language: "en",
|
||||
datetime: { timeZone: "UTC" },
|
||||
language: 'en',
|
||||
datetime: { timeZone: 'UTC' }
|
||||
},
|
||||
alarmEmailsEnabled: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
timeZone: "UTC",
|
||||
language: 'en',
|
||||
timeZone: 'UTC',
|
||||
isBrowserDefaultTimeZone: false,
|
||||
view: "settings",
|
||||
view: 'settings',
|
||||
businessHours: {
|
||||
start: "8:0",
|
||||
end: "19:0",
|
||||
daysOfWeek: [1, 2, 3, 4, 5],
|
||||
start: '8:0',
|
||||
end: '19:0',
|
||||
daysOfWeek: [1, 2, 3, 4, 5]
|
||||
},
|
||||
workingDays: false,
|
||||
},
|
||||
};
|
||||
workingDays: false
|
||||
}
|
||||
}
|
||||
|
||||
describe("Working Days and Business Hours Settings", () => {
|
||||
describe('Working Days and Business Hours Settings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
;(api.patch as jest.Mock).mockResolvedValue({ status: 204 })
|
||||
})
|
||||
|
||||
describe("Initial rendering", () => {
|
||||
it("renders the working days section with day selector", () => {
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||
describe('Initial rendering', () => {
|
||||
it('renders the working days section with day selector', () => {
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState)
|
||||
|
||||
expect(screen.getByText('settings.chooseWorkingDays')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the show only working days toggle', () => {
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState)
|
||||
|
||||
expect(
|
||||
screen.getByText("settings.chooseWorkingDays")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getByRole('switch', { name: /settings.showOnlyWorkingDays/i })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders the show only working days toggle", () => {
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||
it('shows working days toggle as unchecked when workingDays is false', () => {
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState)
|
||||
|
||||
expect(
|
||||
screen.getByRole("switch", { name: /settings.showOnlyWorkingDays/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
const toggle = screen.getByRole('switch', {
|
||||
name: /settings.showOnlyWorkingDays/i
|
||||
}) as HTMLInputElement
|
||||
expect(toggle.checked).toBe(false)
|
||||
})
|
||||
|
||||
it("shows working days toggle as unchecked when workingDays is false", () => {
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||
|
||||
const toggle = screen.getByRole("switch", {
|
||||
name: /settings.showOnlyWorkingDays/i,
|
||||
}) as HTMLInputElement;
|
||||
expect(toggle.checked).toBe(false);
|
||||
});
|
||||
|
||||
it("shows working days toggle as checked when workingDays is true", () => {
|
||||
it('shows working days toggle as checked when workingDays is true', () => {
|
||||
renderWithProviders(<SettingsPage />, {
|
||||
...basePreloadedState,
|
||||
settings: { ...basePreloadedState.settings, workingDays: true },
|
||||
});
|
||||
settings: { ...basePreloadedState.settings, workingDays: true }
|
||||
})
|
||||
|
||||
const toggle = screen.getByRole("switch", {
|
||||
name: /settings.showOnlyWorkingDays/i,
|
||||
}) as HTMLInputElement;
|
||||
expect(toggle.checked).toBe(true);
|
||||
});
|
||||
const toggle = screen.getByRole('switch', {
|
||||
name: /settings.showOnlyWorkingDays/i
|
||||
}) as HTMLInputElement
|
||||
expect(toggle.checked).toBe(true)
|
||||
})
|
||||
|
||||
it("shows working days toggle as unchecked when workingDays is null", () => {
|
||||
it('shows working days toggle as unchecked when workingDays is null', () => {
|
||||
renderWithProviders(<SettingsPage />, {
|
||||
...basePreloadedState,
|
||||
settings: { ...basePreloadedState.settings, workingDays: null },
|
||||
});
|
||||
settings: { ...basePreloadedState.settings, workingDays: null }
|
||||
})
|
||||
|
||||
const toggle = screen.getByRole("switch", {
|
||||
name: /settings.showOnlyWorkingDays/i,
|
||||
}) as HTMLInputElement;
|
||||
expect(toggle.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
const toggle = screen.getByRole('switch', {
|
||||
name: /settings.showOnlyWorkingDays/i
|
||||
}) as HTMLInputElement
|
||||
expect(toggle.checked).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("workingDays toggle", () => {
|
||||
it("updates workingDays immediately (optimistic update) and calls API", async () => {
|
||||
describe('workingDays toggle', () => {
|
||||
it('updates workingDays immediately (optimistic update) and calls API', async () => {
|
||||
const { store } = renderWithProviders(
|
||||
<SettingsPage />,
|
||||
basePreloadedState
|
||||
);
|
||||
)
|
||||
|
||||
const toggle = screen.getByRole("switch", {
|
||||
name: /settings.showOnlyWorkingDays/i,
|
||||
});
|
||||
fireEvent.click(toggle);
|
||||
const toggle = screen.getByRole('switch', {
|
||||
name: /settings.showOnlyWorkingDays/i
|
||||
})
|
||||
fireEvent.click(toggle)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().settings.workingDays).toBe(true);
|
||||
});
|
||||
expect(store.getState().settings.workingDays).toBe(true)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
"api/configurations?scope=user",
|
||||
'api/configurations?scope=user',
|
||||
expect.objectContaining({
|
||||
json: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "linagora.esn.calendar",
|
||||
name: 'linagora.esn.calendar',
|
||||
configurations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "workingDays",
|
||||
value: true,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
name: 'workingDays',
|
||||
value: true
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("rolls back workingDays if API call fails", async () => {
|
||||
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
|
||||
it('rolls back workingDays if API call fails', async () => {
|
||||
;(api.patch as jest.Mock).mockRejectedValue(new Error('API Error'))
|
||||
|
||||
const { store } = renderWithProviders(
|
||||
<SettingsPage />,
|
||||
basePreloadedState
|
||||
);
|
||||
)
|
||||
|
||||
const toggle = screen.getByRole("switch", {
|
||||
name: /settings.showOnlyWorkingDays/i,
|
||||
});
|
||||
fireEvent.click(toggle);
|
||||
const toggle = screen.getByRole('switch', {
|
||||
name: /settings.showOnlyWorkingDays/i
|
||||
})
|
||||
fireEvent.click(toggle)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(store.getState().settings.workingDays).toBe(false);
|
||||
expect(store.getState().settings.workingDays).toBe(false)
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("settings.workingDaysUpdateError")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
screen.getByText('settings.workingDaysUpdateError')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it("toggles workingDays from true to false", async () => {
|
||||
it('toggles workingDays from true to false', async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, {
|
||||
...basePreloadedState,
|
||||
settings: { ...basePreloadedState.settings, workingDays: true },
|
||||
});
|
||||
settings: { ...basePreloadedState.settings, workingDays: true }
|
||||
})
|
||||
|
||||
const toggle = screen.getByRole("switch", {
|
||||
name: /settings.showOnlyWorkingDays/i,
|
||||
});
|
||||
fireEvent.click(toggle);
|
||||
const toggle = screen.getByRole('switch', {
|
||||
name: /settings.showOnlyWorkingDays/i
|
||||
})
|
||||
fireEvent.click(toggle)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().settings.workingDays).toBe(false);
|
||||
});
|
||||
expect(store.getState().settings.workingDays).toBe(false)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
"api/configurations?scope=user",
|
||||
'api/configurations?scope=user',
|
||||
expect.objectContaining({
|
||||
json: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "linagora.esn.calendar",
|
||||
name: 'linagora.esn.calendar',
|
||||
configurations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "workingDays",
|
||||
value: false,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
name: 'workingDays',
|
||||
value: false
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("businessHours day selector", () => {
|
||||
it("updates businessHours in Redux immediately when a day is clicked", async () => {
|
||||
describe('businessHours day selector', () => {
|
||||
it('updates businessHours in Redux immediately when a day is clicked', async () => {
|
||||
const { store } = renderWithProviders(
|
||||
<SettingsPage />,
|
||||
basePreloadedState
|
||||
);
|
||||
)
|
||||
|
||||
// Sunday (0) is not in initial daysOfWeek [1,2,3,4,5], click it to add
|
||||
const sundayButton = screen.getByRole("button", {
|
||||
name: /sunday/i,
|
||||
});
|
||||
fireEvent.click(sundayButton);
|
||||
const sundayButton = screen.getByRole('button', {
|
||||
name: /sunday/i
|
||||
})
|
||||
fireEvent.click(sundayButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().settings.businessHours?.daysOfWeek).toContain(
|
||||
0
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(store.getState().settings.businessHours?.daysOfWeek).toContain(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("removes a day from businessHours when an already selected day is clicked", async () => {
|
||||
it('removes a day from businessHours when an already selected day is clicked', async () => {
|
||||
const { store } = renderWithProviders(
|
||||
<SettingsPage />,
|
||||
basePreloadedState
|
||||
);
|
||||
)
|
||||
|
||||
// Monday (1) is in initial daysOfWeek, click to remove
|
||||
const mondayButton = screen.getByRole("button", { name: /monday/i });
|
||||
fireEvent.click(mondayButton);
|
||||
const mondayButton = screen.getByRole('button', { name: /monday/i })
|
||||
fireEvent.click(mondayButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
store.getState().settings.businessHours?.daysOfWeek
|
||||
).not.toContain(1);
|
||||
});
|
||||
});
|
||||
).not.toContain(1)
|
||||
})
|
||||
})
|
||||
|
||||
it("debounces API call when multiple days are clicked rapidly", async () => {
|
||||
jest.useFakeTimers();
|
||||
it('debounces API call when multiple days are clicked rapidly', async () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState)
|
||||
|
||||
const sundayButton = screen.getByRole("button", { name: /sunday/i });
|
||||
const saturdayButton = screen.getByRole("button", { name: /saturday/i });
|
||||
const sundayButton = screen.getByRole('button', { name: /sunday/i })
|
||||
const saturdayButton = screen.getByRole('button', { name: /saturday/i })
|
||||
|
||||
fireEvent.click(sundayButton);
|
||||
fireEvent.click(saturdayButton);
|
||||
fireEvent.click(sundayButton);
|
||||
fireEvent.click(sundayButton)
|
||||
fireEvent.click(saturdayButton)
|
||||
fireEvent.click(sundayButton)
|
||||
|
||||
// API should not have been called yet
|
||||
expect(api.patch).not.toHaveBeenCalled();
|
||||
expect(api.patch).not.toHaveBeenCalled()
|
||||
|
||||
// Advance timers past debounce threshold
|
||||
jest.runAllTimers();
|
||||
jest.runAllTimers()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.patch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(api.patch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it("sends businessHours as array to API", async () => {
|
||||
jest.useFakeTimers();
|
||||
it('sends businessHours as array to API', async () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||
renderWithProviders(<SettingsPage />, basePreloadedState)
|
||||
|
||||
const sundayButton = screen.getByRole("button", { name: /sunday/i });
|
||||
fireEvent.click(sundayButton);
|
||||
const sundayButton = screen.getByRole('button', { name: /sunday/i })
|
||||
fireEvent.click(sundayButton)
|
||||
|
||||
jest.runAllTimers();
|
||||
jest.runAllTimers()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
"api/configurations?scope=user",
|
||||
'api/configurations?scope=user',
|
||||
expect.objectContaining({
|
||||
json: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "businessHours",
|
||||
name: 'businessHours',
|
||||
value: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
start: "8:0",
|
||||
end: "19:0",
|
||||
daysOfWeek: expect.arrayContaining([0]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
start: '8:0',
|
||||
end: '19:0',
|
||||
daysOfWeek: expect.arrayContaining([0])
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it("rolls back businessHours if API call fails", async () => {
|
||||
jest.useFakeTimers();
|
||||
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
|
||||
it('rolls back businessHours if API call fails', async () => {
|
||||
jest.useFakeTimers()
|
||||
;(api.patch as jest.Mock).mockRejectedValue(new Error('API Error'))
|
||||
|
||||
const { store } = renderWithProviders(
|
||||
<SettingsPage />,
|
||||
basePreloadedState
|
||||
);
|
||||
)
|
||||
|
||||
const sundayButton = screen.getByRole("button", { name: /sunday/i });
|
||||
fireEvent.click(sundayButton);
|
||||
const sundayButton = screen.getByRole('button', { name: /sunday/i })
|
||||
fireEvent.click(sundayButton)
|
||||
|
||||
jest.runAllTimers();
|
||||
jest.runAllTimers()
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
store.getState().settings.businessHours?.daysOfWeek
|
||||
).not.toContain(0);
|
||||
).not.toContain(0)
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("settings.workingDaysUpdateError")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getByText('settings.workingDaysUpdateError')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOpenPaasUserDataAsync integration", () => {
|
||||
it("populates businessHours from core module configurations", async () => {
|
||||
describe('getOpenPaasUserDataAsync integration', () => {
|
||||
it('populates businessHours from core module configurations', async () => {
|
||||
const store = configureStore({
|
||||
reducer: { user: userReducer, settings: settingsReducer },
|
||||
preloadedState: {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
name: 'John',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
|
||||
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
|
||||
alarmEmailsEnabled: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
timeZone: "UTC",
|
||||
language: 'en',
|
||||
timeZone: 'UTC',
|
||||
isBrowserDefaultTimeZone: false,
|
||||
view: "calendar",
|
||||
view: 'calendar',
|
||||
businessHours: null,
|
||||
workingDays: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
workingDays: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mockResponse = {
|
||||
id: "667037022b752d0026472254",
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
preferredEmail: "test@test.com",
|
||||
id: '667037022b752d0026472254',
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
preferredEmail: 'test@test.com',
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: [
|
||||
{
|
||||
name: "businessHours",
|
||||
name: 'businessHours',
|
||||
value: [
|
||||
{ start: "8:0", end: "19:0", daysOfWeek: [1, 2, 3, 4, 5] },
|
||||
],
|
||||
},
|
||||
],
|
||||
{ start: '8:0', end: '19:0', daysOfWeek: [1, 2, 3, 4, 5] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "linagora.esn.calendar",
|
||||
configurations: [{ name: "workingDays", value: true }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
name: 'linagora.esn.calendar',
|
||||
configurations: [{ name: 'workingDays', value: true }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
|
||||
);
|
||||
getOpenPaasUserDataAsync.fulfilled(mockResponse, '', undefined)
|
||||
)
|
||||
|
||||
const state = store.getState();
|
||||
const state = store.getState()
|
||||
expect(state.settings.businessHours).toEqual({
|
||||
start: "8:0",
|
||||
end: "19:0",
|
||||
daysOfWeek: [1, 2, 3, 4, 5],
|
||||
});
|
||||
expect(state.settings.workingDays).toBe(true);
|
||||
});
|
||||
start: '8:0',
|
||||
end: '19:0',
|
||||
daysOfWeek: [1, 2, 3, 4, 5]
|
||||
})
|
||||
expect(state.settings.workingDays).toBe(true)
|
||||
})
|
||||
|
||||
it("sets businessHours to null when not present in API response", async () => {
|
||||
it('sets businessHours to null when not present in API response', async () => {
|
||||
const store = configureStore({
|
||||
reducer: { user: userReducer, settings: settingsReducer },
|
||||
preloadedState: {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
name: 'John',
|
||||
sid: 'mockSid',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
|
||||
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
|
||||
alarmEmailsEnabled: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
timeZone: "UTC",
|
||||
language: 'en',
|
||||
timeZone: 'UTC',
|
||||
isBrowserDefaultTimeZone: false,
|
||||
view: "calendar",
|
||||
view: 'calendar',
|
||||
businessHours: null,
|
||||
workingDays: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
workingDays: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mockResponse = {
|
||||
id: "667037022b752d0026472254",
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
preferredEmail: "test@test.com",
|
||||
id: '667037022b752d0026472254',
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
preferredEmail: 'test@test.com',
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [],
|
||||
name: 'core',
|
||||
configurations: []
|
||||
},
|
||||
{
|
||||
name: "linagora.esn.calendar",
|
||||
configurations: [{ name: "workingDays", value: null }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
name: 'linagora.esn.calendar',
|
||||
configurations: [{ name: 'workingDays', value: null }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
|
||||
);
|
||||
getOpenPaasUserDataAsync.fulfilled(mockResponse, '', undefined)
|
||||
)
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.settings.businessHours).toBeNull();
|
||||
expect(state.settings.workingDays).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
const state = store.getState()
|
||||
expect(state.settings.businessHours).toBeNull()
|
||||
expect(state.settings.workingDays).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import * as appHooks from "@/app/hooks";
|
||||
import { AppDispatch, setupStore } from "@/app/store";
|
||||
import HandleLogin from "@/features/User/HandleLogin";
|
||||
import * as oidcAuth from "@/features/User/oidcAuth";
|
||||
import { clientConfig } from "@/features/User/oidcAuth";
|
||||
import { useInitializeApp } from "@/features/User/useInitializeApp";
|
||||
import * as apiUtils from "@/utils/apiUtils";
|
||||
import { renderHook, screen, waitFor } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { push } from "redux-first-history";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import * as appHooks from '@/app/hooks'
|
||||
import { AppDispatch, setupStore } from '@/app/store'
|
||||
import HandleLogin from '@/features/User/HandleLogin'
|
||||
import * as oidcAuth from '@/features/User/oidcAuth'
|
||||
import { clientConfig } from '@/features/User/oidcAuth'
|
||||
import { useInitializeApp } from '@/features/User/useInitializeApp'
|
||||
import * as apiUtils from '@/utils/apiUtils'
|
||||
import { renderHook, screen, waitFor } from '@testing-library/react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { push } from 'redux-first-history'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
|
||||
clientConfig.url = "https://example.com";
|
||||
clientConfig.url = 'https://example.com'
|
||||
|
||||
describe("HandleLogin", () => {
|
||||
describe('HandleLogin', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(apiUtils, "redirectTo").mockImplementation(() => {});
|
||||
const dispatch = jest.fn() as AppDispatch;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
sessionStorage.clear();
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
jest.clearAllMocks()
|
||||
jest.spyOn(apiUtils, 'redirectTo').mockImplementation(() => {})
|
||||
const dispatch = jest.fn() as AppDispatch
|
||||
jest.spyOn(appHooks, 'useAppDispatch').mockReturnValue(dispatch)
|
||||
sessionStorage.clear()
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
dispatchEvent: jest.fn()
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
test("redirects and sets sessionStorage when no userData", async () => {
|
||||
test('redirects and sets sessionStorage when no userData', async () => {
|
||||
const loginUrlMock = {
|
||||
code_verifier: "verifier123",
|
||||
state: "state123",
|
||||
redirectTo: new URL("http://login.url"),
|
||||
};
|
||||
code_verifier: 'verifier123',
|
||||
state: 'state123',
|
||||
redirectTo: new URL('http://login.url')
|
||||
}
|
||||
|
||||
jest.spyOn(oidcAuth, "Auth").mockResolvedValue(loginUrlMock);
|
||||
jest.spyOn(oidcAuth, 'Auth').mockResolvedValue(loginUrlMock)
|
||||
|
||||
const { result } = renderHook(() => useInitializeApp(), {
|
||||
wrapper: ({ children }) => (
|
||||
@@ -50,118 +50,118 @@ describe("HandleLogin", () => {
|
||||
tokens: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
coreConfig: { language: "en" },
|
||||
coreConfig: { language: 'en' }
|
||||
},
|
||||
calendars: { list: {}, pending: false, error: null },
|
||||
calendars: { list: {}, pending: false, error: null }
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(oidcAuth.Auth).toHaveBeenCalled();
|
||||
expect(oidcAuth.Auth).toHaveBeenCalled()
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(sessionStorage.getItem("redirectState")).toEqual(
|
||||
expect(sessionStorage.getItem('redirectState')).toEqual(
|
||||
JSON.stringify({
|
||||
code_verifier: "verifier123",
|
||||
state: "state123",
|
||||
code_verifier: 'verifier123',
|
||||
state: 'state123'
|
||||
})
|
||||
);
|
||||
)
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(apiUtils.redirectTo).toHaveBeenCalledWith(
|
||||
loginUrlMock.redirectTo
|
||||
);
|
||||
)
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
test("does not render loading element when userData exists and calendars pending is true", () => {
|
||||
test('does not render loading element when userData exists and calendars pending is true', () => {
|
||||
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'
|
||||
},
|
||||
tokens: { access_token: "test" },
|
||||
loading: false,
|
||||
tokens: { access_token: 'test' },
|
||||
loading: false
|
||||
},
|
||||
calendars: { list: {}, pending: true },
|
||||
loading: { isLoading: true },
|
||||
};
|
||||
loading: { isLoading: true }
|
||||
}
|
||||
|
||||
renderWithProviders(<HandleLogin />, preloadedState);
|
||||
renderWithProviders(<HandleLogin />, preloadedState)
|
||||
// HandleLogin now returns null, loading is shown at App level via appLoading state
|
||||
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
|
||||
});
|
||||
test("does not render loading element when userData exists and calendars pending is false", () => {
|
||||
expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
|
||||
})
|
||||
test('does not render loading element when userData exists and calendars pending is false', () => {
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "cmoussu",
|
||||
email: "cmoussu@linagora.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
sub: 'cmoussu',
|
||||
email: 'cmoussu@linagora.com',
|
||||
sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro',
|
||||
openpaasId: '667037022b752d0026472254'
|
||||
},
|
||||
tokens: { access_token: "test" },
|
||||
loading: false,
|
||||
tokens: { access_token: 'test' },
|
||||
loading: false
|
||||
},
|
||||
calendars: { list: {}, pending: false },
|
||||
loading: { isLoading: false },
|
||||
};
|
||||
renderWithProviders(<HandleLogin />, preloadedState);
|
||||
loading: { isLoading: false }
|
||||
}
|
||||
renderWithProviders(<HandleLogin />, preloadedState)
|
||||
|
||||
// HandleLogin now returns null, loading is shown at App level via appLoading state
|
||||
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
|
||||
});
|
||||
test("goes to error page when there is error in user data", async () => {
|
||||
const mockDispatch = jest.fn();
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(mockDispatch);
|
||||
jest.spyOn(oidcAuth, "Auth").mockResolvedValue({
|
||||
code_verifier: "verifier123",
|
||||
state: "state123",
|
||||
redirectTo: new URL("http://login.url"),
|
||||
});
|
||||
expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
|
||||
})
|
||||
test('goes to error page when there is error in user data', async () => {
|
||||
const mockDispatch = jest.fn()
|
||||
jest.spyOn(appHooks, 'useAppDispatch').mockReturnValue(mockDispatch)
|
||||
jest.spyOn(oidcAuth, 'Auth').mockResolvedValue({
|
||||
code_verifier: 'verifier123',
|
||||
state: 'state123',
|
||||
redirectTo: new URL('http://login.url')
|
||||
})
|
||||
|
||||
renderWithProviders(<HandleLogin />, {
|
||||
user: {
|
||||
error: true,
|
||||
loading: false,
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "testSid",
|
||||
openpaasId: "testId",
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
sid: 'testSid',
|
||||
openpaasId: 'testId'
|
||||
},
|
||||
tokens: { access_token: "test" },
|
||||
tokens: { access_token: 'test' }
|
||||
},
|
||||
calendars: {
|
||||
list: {},
|
||||
pending: false,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
error: null
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(mockDispatch).toHaveBeenCalledWith(push("/error"));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(push('/error'))
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,183 +1,182 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { getCalendarsListAsync } from "@/features/Calendars/services/getCalendarsListAsync";
|
||||
import { CallbackResume } from "@/features/User/LoginCallback";
|
||||
import * as oidcAuth from "@/features/User/oidcAuth";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { getCalendarsListAsync } from '@/features/Calendars/services/getCalendarsListAsync'
|
||||
import { CallbackResume } from '@/features/User/LoginCallback'
|
||||
import * as oidcAuth from '@/features/User/oidcAuth'
|
||||
import {
|
||||
getOpenPaasUserDataAsync,
|
||||
setTokens,
|
||||
setUserData,
|
||||
} from "@/features/User/userSlice";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { replace } from "redux-first-history";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import { setAppLoading } from "@/app/loadingSlice";
|
||||
setUserData
|
||||
} from '@/features/User/userSlice'
|
||||
import { render, waitFor } from '@testing-library/react'
|
||||
import { replace } from 'redux-first-history'
|
||||
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
||||
import { setAppLoading } from '@/app/loadingSlice'
|
||||
|
||||
// Mocks
|
||||
jest.mock("@/app/hooks", () => ({
|
||||
jest.mock('@/app/hooks', () => ({
|
||||
useAppDispatch: jest.fn(),
|
||||
useAppSelector: jest.fn(() => ({
|
||||
user: {
|
||||
userData: null,
|
||||
tokens: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
error: null
|
||||
},
|
||||
calendars: {
|
||||
list: {},
|
||||
pending: false,
|
||||
error: null,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
error: null
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
jest.mock("@/features/User/oidcAuth", () => ({
|
||||
Callback: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/features/User/oidcAuth', () => ({
|
||||
Callback: jest.fn()
|
||||
}))
|
||||
|
||||
jest.mock("@/features/User/userSlice", () => {
|
||||
jest.mock('@/features/User/userSlice', () => {
|
||||
const mockGetUser = Object.assign(
|
||||
jest.fn(() => ({ type: "GET_USER_ID" })),
|
||||
jest.fn(() => ({ type: 'GET_USER_ID' })),
|
||||
{
|
||||
pending: { type: "GET_USER_ID/pending" },
|
||||
fulfilled: { type: "GET_USER_ID/fulfilled" },
|
||||
rejected: { type: "GET_USER_ID/rejected" },
|
||||
pending: { type: 'GET_USER_ID/pending' },
|
||||
fulfilled: { type: 'GET_USER_ID/fulfilled' },
|
||||
rejected: { type: 'GET_USER_ID/rejected' }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
return {
|
||||
setUserData: jest.fn((data) => ({ type: "SET_USER", payload: data })),
|
||||
setTokens: jest.fn((tokens) => ({ type: "SET_TOKENS", payload: tokens })),
|
||||
getOpenPaasUserDataAsync: mockGetUser,
|
||||
};
|
||||
});
|
||||
setUserData: jest.fn(data => ({ type: 'SET_USER', payload: data })),
|
||||
setTokens: jest.fn(tokens => ({ type: 'SET_TOKENS', payload: tokens })),
|
||||
getOpenPaasUserDataAsync: mockGetUser
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock("@/features/Calendars/services/getCalendarsListAsync", () => {
|
||||
jest.mock('@/features/Calendars/services/getCalendarsListAsync', () => {
|
||||
const mockGetCalendars = Object.assign(
|
||||
jest.fn(() => ({ type: "GET_CALENDARS" })),
|
||||
jest.fn(() => ({ type: 'GET_CALENDARS' })),
|
||||
{
|
||||
pending: { type: "GET_CALENDARS/pending" },
|
||||
fulfilled: { type: "GET_CALENDARS/fulfilled" },
|
||||
rejected: { type: "GET_CALENDARS/rejected" },
|
||||
pending: { type: 'GET_CALENDARS/pending' },
|
||||
fulfilled: { type: 'GET_CALENDARS/fulfilled' },
|
||||
rejected: { type: 'GET_CALENDARS/rejected' }
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
return {
|
||||
getCalendarsListAsync: mockGetCalendars,
|
||||
};
|
||||
});
|
||||
getCalendarsListAsync: mockGetCalendars
|
||||
}
|
||||
})
|
||||
|
||||
describe("CallbackResume", () => {
|
||||
const dispatch = jest.fn();
|
||||
let mockUserState: any;
|
||||
let mockCalendarsState: any;
|
||||
describe('CallbackResume', () => {
|
||||
const dispatch = jest.fn()
|
||||
let mockUserState: any
|
||||
let mockCalendarsState: any
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useAppDispatch as unknown as jest.Mock).mockReturnValue(dispatch);
|
||||
jest.clearAllMocks()
|
||||
;(useAppDispatch as unknown as jest.Mock).mockReturnValue(dispatch)
|
||||
|
||||
// Initialize mock states
|
||||
mockUserState = {
|
||||
userData: null,
|
||||
tokens: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
error: null
|
||||
}
|
||||
mockCalendarsState = {
|
||||
list: {},
|
||||
pending: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
(useAppSelector as jest.Mock).mockImplementation((selector) => {
|
||||
error: null
|
||||
}
|
||||
;(useAppSelector as jest.Mock).mockImplementation(selector => {
|
||||
const state = {
|
||||
user: mockUserState,
|
||||
calendars: mockCalendarsState,
|
||||
};
|
||||
return selector(state);
|
||||
});
|
||||
});
|
||||
calendars: mockCalendarsState
|
||||
}
|
||||
return selector(state)
|
||||
})
|
||||
})
|
||||
|
||||
it("should call Callback and dispatch necessary actions", async () => {
|
||||
const mockTokenSet = { access_token: "abc" };
|
||||
const mockUserInfo = { name: "Test User" };
|
||||
it('should call Callback and dispatch necessary actions', async () => {
|
||||
const mockTokenSet = { access_token: 'abc' }
|
||||
const mockUserInfo = { name: 'Test User' }
|
||||
|
||||
const mockData = {
|
||||
tokenSet: mockTokenSet,
|
||||
userinfo: mockUserInfo,
|
||||
};
|
||||
userinfo: mockUserInfo
|
||||
}
|
||||
|
||||
(oidcAuth.Callback as jest.Mock).mockResolvedValue(mockData);
|
||||
;(oidcAuth.Callback as jest.Mock).mockResolvedValue(mockData)
|
||||
|
||||
sessionStorage.setItem(
|
||||
"redirectState",
|
||||
JSON.stringify({ code_verifier: "verifier123", state: "state456" })
|
||||
);
|
||||
'redirectState',
|
||||
JSON.stringify({ code_verifier: 'verifier123', state: 'state456' })
|
||||
)
|
||||
|
||||
const { rerender } = render(<CallbackResume />);
|
||||
const { rerender } = render(<CallbackResume />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(oidcAuth.Callback).toHaveBeenCalledWith("verifier123", "state456");
|
||||
});
|
||||
expect(oidcAuth.Callback).toHaveBeenCalledWith('verifier123', 'state456')
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(setAppLoading(true));
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(setAppLoading(true))
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(setUserData(mockUserInfo));
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(setUserData(mockUserInfo))
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(setTokens(mockTokenSet));
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(setTokens(mockTokenSet))
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(getOpenPaasUserDataAsync());
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(getOpenPaasUserDataAsync())
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(getCalendarsListAsync());
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(getCalendarsListAsync())
|
||||
})
|
||||
|
||||
// Simulate async actions completing by updating mock state
|
||||
mockUserState = {
|
||||
userData: mockUserInfo,
|
||||
tokens: mockTokenSet,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
error: null
|
||||
}
|
||||
mockCalendarsState = {
|
||||
list: { calendar1: {} },
|
||||
pending: false,
|
||||
error: null,
|
||||
};
|
||||
error: null
|
||||
}
|
||||
|
||||
// Re-render to trigger navigation effect
|
||||
rerender(<CallbackResume />);
|
||||
rerender(<CallbackResume />)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(setAppLoading(false));
|
||||
expect(dispatch).toHaveBeenCalledWith(setAppLoading(false))
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(replace("/calendar"));
|
||||
expect(dispatch).toHaveBeenCalledWith(replace('/calendar'))
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.getItem("redirectState")).toBe(null);
|
||||
});
|
||||
expect(sessionStorage.getItem('redirectState')).toBe(null)
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.getItem("tokenSet")).toEqual(
|
||||
expect(sessionStorage.getItem('tokenSet')).toEqual(
|
||||
JSON.stringify(mockTokenSet)
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle missing redirectState gracefully", async () => {
|
||||
sessionStorage.removeItem("redirectState");
|
||||
renderWithProviders(<CallbackResume />);
|
||||
it('should handle missing redirectState gracefully', async () => {
|
||||
sessionStorage.removeItem('redirectState')
|
||||
renderWithProviders(<CallbackResume />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(replace("/"));
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(replace('/'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,169 +4,168 @@ import {
|
||||
Callback,
|
||||
clientConfig,
|
||||
getClientConfig,
|
||||
Logout,
|
||||
} from "@/features/User/oidcAuth";
|
||||
import * as apiUtils from "@/utils/apiUtils";
|
||||
import * as client from "openid-client";
|
||||
Logout
|
||||
} from '@/features/User/oidcAuth'
|
||||
import * as apiUtils from '@/utils/apiUtils'
|
||||
import * as client from 'openid-client'
|
||||
|
||||
clientConfig.url = "https://example.com";
|
||||
const localAdress = "https://local.exemple.com";
|
||||
clientConfig.url = 'https://example.com'
|
||||
const localAdress = 'https://local.exemple.com'
|
||||
|
||||
describe("OpenID Client Auth Module", () => {
|
||||
describe('OpenID Client Auth Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(apiUtils, "getLocation").mockImplementation(() => localAdress);
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
jest.spyOn(apiUtils, 'getLocation').mockImplementation(() => localAdress)
|
||||
})
|
||||
|
||||
describe("getClientConfig", () => {
|
||||
it("should call discovery with clientConfig.url", async () => {
|
||||
const discoveryMock = client.discovery as jest.Mock;
|
||||
discoveryMock.mockResolvedValue("discoveredClient");
|
||||
describe('getClientConfig', () => {
|
||||
it('should call discovery with clientConfig.url', async () => {
|
||||
const discoveryMock = client.discovery as jest.Mock
|
||||
discoveryMock.mockResolvedValue('discoveredClient')
|
||||
|
||||
const result = await getClientConfig();
|
||||
const result = await getClientConfig()
|
||||
|
||||
expect(discoveryMock).toHaveBeenCalledWith(
|
||||
new URL(clientConfig.url),
|
||||
clientConfig.client_id
|
||||
);
|
||||
expect(result).toBe("discoveredClient");
|
||||
});
|
||||
});
|
||||
)
|
||||
expect(result).toBe('discoveredClient')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Auth", () => {
|
||||
it("should generate PKCE and build authorization URL with PKCE", async () => {
|
||||
(client.randomPKCECodeVerifier as jest.Mock).mockReturnValue(
|
||||
"verifier123"
|
||||
);
|
||||
(client.calculatePKCECodeChallenge as jest.Mock).mockResolvedValue(
|
||||
"challenge123"
|
||||
);
|
||||
describe('Auth', () => {
|
||||
it('should generate PKCE and build authorization URL with PKCE', async () => {
|
||||
;(client.randomPKCECodeVerifier as jest.Mock).mockReturnValue(
|
||||
'verifier123'
|
||||
)
|
||||
;(client.calculatePKCECodeChallenge as jest.Mock).mockResolvedValue(
|
||||
'challenge123'
|
||||
)
|
||||
|
||||
// Mock discovery returning an object with serverMetadata()
|
||||
const discoveredClient = {
|
||||
serverMetadata: jest.fn(() => ({
|
||||
supportsPKCE: () => true,
|
||||
})),
|
||||
};
|
||||
(client.discovery as jest.Mock).mockResolvedValue(discoveredClient);
|
||||
supportsPKCE: () => true
|
||||
}))
|
||||
}
|
||||
;(client.discovery as jest.Mock).mockResolvedValue(discoveredClient)
|
||||
;(client.buildAuthorizationUrl as jest.Mock).mockReturnValue(
|
||||
'https://auth.url'
|
||||
)
|
||||
|
||||
(client.buildAuthorizationUrl as jest.Mock).mockReturnValue(
|
||||
"https://auth.url"
|
||||
);
|
||||
const result = await Auth()
|
||||
|
||||
const result = await Auth();
|
||||
|
||||
expect(client.randomPKCECodeVerifier).toHaveBeenCalled();
|
||||
expect(client.randomPKCECodeVerifier).toHaveBeenCalled()
|
||||
expect(client.calculatePKCECodeChallenge).toHaveBeenCalledWith(
|
||||
"verifier123"
|
||||
);
|
||||
'verifier123'
|
||||
)
|
||||
expect(client.buildAuthorizationUrl).toHaveBeenCalledWith(
|
||||
discoveredClient,
|
||||
expect.objectContaining({
|
||||
code_challenge: "challenge123",
|
||||
code_challenge: 'challenge123',
|
||||
code_challenge_method: clientConfig.code_challenge_method,
|
||||
redirect_uri: clientConfig.redirect_uri,
|
||||
scope: clientConfig.scope,
|
||||
scope: clientConfig.scope
|
||||
})
|
||||
);
|
||||
)
|
||||
expect(result).toEqual({
|
||||
redirectTo: "https://auth.url",
|
||||
code_verifier: "verifier123",
|
||||
state: undefined,
|
||||
});
|
||||
});
|
||||
redirectTo: 'https://auth.url',
|
||||
code_verifier: 'verifier123',
|
||||
state: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it("should generate state when PKCE not supported", async () => {
|
||||
(client.randomPKCECodeVerifier as jest.Mock).mockReturnValue(
|
||||
"verifier123"
|
||||
);
|
||||
(client.calculatePKCECodeChallenge as jest.Mock).mockResolvedValue(
|
||||
"challenge123"
|
||||
);
|
||||
it('should generate state when PKCE not supported', async () => {
|
||||
;(client.randomPKCECodeVerifier as jest.Mock).mockReturnValue(
|
||||
'verifier123'
|
||||
)
|
||||
;(client.calculatePKCECodeChallenge as jest.Mock).mockResolvedValue(
|
||||
'challenge123'
|
||||
)
|
||||
|
||||
const discoveredClient = {
|
||||
serverMetadata: jest.fn(() => ({
|
||||
supportsPKCE: () => false,
|
||||
})),
|
||||
};
|
||||
(client.discovery as jest.Mock).mockResolvedValue(discoveredClient);
|
||||
(client.randomState as jest.Mock).mockReturnValue("state123");
|
||||
(client.buildAuthorizationUrl as jest.Mock).mockReturnValue(
|
||||
"https://auth.url"
|
||||
);
|
||||
supportsPKCE: () => false
|
||||
}))
|
||||
}
|
||||
;(client.discovery as jest.Mock).mockResolvedValue(discoveredClient)
|
||||
;(client.randomState as jest.Mock).mockReturnValue('state123')
|
||||
;(client.buildAuthorizationUrl as jest.Mock).mockReturnValue(
|
||||
'https://auth.url'
|
||||
)
|
||||
|
||||
const result = await Auth();
|
||||
const result = await Auth()
|
||||
|
||||
expect(client.randomState).toHaveBeenCalled();
|
||||
expect(result.state).toBe("state123");
|
||||
expect(result.redirectTo).toBe("https://auth.url");
|
||||
});
|
||||
});
|
||||
expect(client.randomState).toHaveBeenCalled()
|
||||
expect(result.state).toBe('state123')
|
||||
expect(result.redirectTo).toBe('https://auth.url')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Logout", () => {
|
||||
it("should build end session URL", async () => {
|
||||
const discoveredClient = {};
|
||||
(client.discovery as jest.Mock).mockResolvedValue(discoveredClient);
|
||||
(client.buildEndSessionUrl as jest.Mock).mockReturnValue(
|
||||
"https://logout.url"
|
||||
);
|
||||
describe('Logout', () => {
|
||||
it('should build end session URL', async () => {
|
||||
const discoveredClient = {}
|
||||
;(client.discovery as jest.Mock).mockResolvedValue(discoveredClient)
|
||||
;(client.buildEndSessionUrl as jest.Mock).mockReturnValue(
|
||||
'https://logout.url'
|
||||
)
|
||||
|
||||
const result = await Logout();
|
||||
const result = await Logout()
|
||||
|
||||
expect(client.buildEndSessionUrl).toHaveBeenCalledWith(discoveredClient, {
|
||||
post_logout_redirect_uri: clientConfig.post_logout_redirect_uri,
|
||||
});
|
||||
expect(result).toBe("https://logout.url");
|
||||
});
|
||||
});
|
||||
post_logout_redirect_uri: clientConfig.post_logout_redirect_uri
|
||||
})
|
||||
expect(result).toBe('https://logout.url')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Callback", () => {
|
||||
it("should perform authorization code grant and fetch user info", async () => {
|
||||
const discoveredClient = {};
|
||||
(client.discovery as jest.Mock).mockResolvedValue(discoveredClient);
|
||||
describe('Callback', () => {
|
||||
it('should perform authorization code grant and fetch user info', async () => {
|
||||
const discoveredClient = {}
|
||||
;(client.discovery as jest.Mock).mockResolvedValue(discoveredClient)
|
||||
|
||||
const mockTokenSet = {
|
||||
access_token: "access123",
|
||||
claims: jest.fn(() => ({ sub: "user123" })),
|
||||
};
|
||||
(client.authorizationCodeGrant as jest.Mock).mockResolvedValue(
|
||||
access_token: 'access123',
|
||||
claims: jest.fn(() => ({ sub: 'user123' }))
|
||||
}
|
||||
;(client.authorizationCodeGrant as jest.Mock).mockResolvedValue(
|
||||
mockTokenSet
|
||||
);
|
||||
(client.fetchUserInfo as jest.Mock).mockResolvedValue({ name: "User" });
|
||||
const result = await Callback("verifier123", "state123");
|
||||
)
|
||||
;(client.fetchUserInfo as jest.Mock).mockResolvedValue({ name: 'User' })
|
||||
const result = await Callback('verifier123', 'state123')
|
||||
|
||||
expect(client.authorizationCodeGrant).toHaveBeenCalledWith(
|
||||
discoveredClient,
|
||||
new URL(localAdress),
|
||||
{ pkceCodeVerifier: "verifier123", expectedState: "state123" }
|
||||
);
|
||||
{ pkceCodeVerifier: 'verifier123', expectedState: 'state123' }
|
||||
)
|
||||
|
||||
expect(client.fetchUserInfo).toHaveBeenCalledWith(
|
||||
discoveredClient,
|
||||
"access123",
|
||||
"user123"
|
||||
);
|
||||
'access123',
|
||||
'user123'
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
tokenSet: mockTokenSet,
|
||||
userinfo: { name: "User" },
|
||||
});
|
||||
});
|
||||
userinfo: { name: 'User' }
|
||||
})
|
||||
})
|
||||
|
||||
it("should catch and log errors", async () => {
|
||||
const error = new Error("fail");
|
||||
(client.discovery as jest.Mock).mockResolvedValue({});
|
||||
(client.authorizationCodeGrant as jest.Mock).mockRejectedValue(error);
|
||||
it('should catch and log errors', async () => {
|
||||
const error = new Error('fail')
|
||||
;(client.discovery as jest.Mock).mockResolvedValue({})
|
||||
;(client.authorizationCodeGrant as jest.Mock).mockRejectedValue(error)
|
||||
const consoleErrorSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const result = await Callback("verifier", "state");
|
||||
const result = await Callback('verifier', 'state')
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Token grant error:", error);
|
||||
expect(result).toBeUndefined();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Token grant error:', error)
|
||||
expect(result).toBeUndefined()
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,114 +1,114 @@
|
||||
import { clientConfig } from "@/features/User/oidcAuth";
|
||||
import { clientConfig } from '@/features/User/oidcAuth'
|
||||
import {
|
||||
getOpenPaasUser,
|
||||
updateUserConfigurations,
|
||||
getResourceDetails,
|
||||
getUserDetails,
|
||||
} from "@/features/User/userAPI";
|
||||
import { api } from "@/utils/apiUtils";
|
||||
getUserDetails
|
||||
} from '@/features/User/userAPI'
|
||||
import { api } from '@/utils/apiUtils'
|
||||
|
||||
jest.mock("@/utils/apiUtils");
|
||||
jest.mock('@/utils/apiUtils')
|
||||
|
||||
clientConfig.url = "https://example.com";
|
||||
clientConfig.url = 'https://example.com'
|
||||
|
||||
describe("getOpenPaasUser", () => {
|
||||
it("should fetch and return user data", async () => {
|
||||
const mockUser = { id: "123", name: "OpenPaas User" };
|
||||
describe('getOpenPaasUser', () => {
|
||||
it('should fetch and return user data', async () => {
|
||||
const mockUser = { id: '123', name: 'OpenPaas User' }
|
||||
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockUser),
|
||||
});
|
||||
;(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockUser)
|
||||
})
|
||||
|
||||
const result = await getOpenPaasUser();
|
||||
const result = await getOpenPaasUser()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith("api/user");
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
expect(api.get).toHaveBeenCalledWith('api/user')
|
||||
expect(result).toEqual(mockUser)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getUserDetails", () => {
|
||||
it("should fetch and return user details", async () => {
|
||||
describe('getUserDetails', () => {
|
||||
it('should fetch and return user details', async () => {
|
||||
const mockUser = {
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
emails: ["john@test.com"],
|
||||
};
|
||||
const userId = "123";
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
emails: ['john@test.com']
|
||||
}
|
||||
const userId = '123'
|
||||
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockUser),
|
||||
});
|
||||
;(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockUser)
|
||||
})
|
||||
|
||||
const result = await getUserDetails(userId);
|
||||
const result = await getUserDetails(userId)
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(`api/users/${userId}`);
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
expect(api.get).toHaveBeenCalledWith(`api/users/${userId}`)
|
||||
expect(result).toEqual(mockUser)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getResourceDetails", () => {
|
||||
it("should fetch and return resource details", async () => {
|
||||
const mockResource = { _id: "res-123", name: "Meeting Room A" };
|
||||
const resourceId = "res-123";
|
||||
describe('getResourceDetails', () => {
|
||||
it('should fetch and return resource details', async () => {
|
||||
const mockResource = { _id: 'res-123', name: 'Meeting Room A' }
|
||||
const resourceId = 'res-123'
|
||||
|
||||
(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockResource),
|
||||
});
|
||||
;(api.get as jest.Mock).mockReturnValue({
|
||||
json: jest.fn().mockResolvedValue(mockResource)
|
||||
})
|
||||
|
||||
const result = await getResourceDetails(resourceId);
|
||||
const result = await getResourceDetails(resourceId)
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(`api/resources/${resourceId}`);
|
||||
expect(result).toEqual(mockResource);
|
||||
});
|
||||
});
|
||||
expect(api.get).toHaveBeenCalledWith(`api/resources/${resourceId}`)
|
||||
expect(result).toEqual(mockResource)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateUserConfigurations", () => {
|
||||
describe('updateUserConfigurations', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should PATCH configurations with language update", async () => {
|
||||
const mockResponse = { status: 204 };
|
||||
(api.patch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
it('should PATCH configurations with language update', async () => {
|
||||
const mockResponse = { status: 204 }
|
||||
;(api.patch as jest.Mock).mockResolvedValue(mockResponse)
|
||||
|
||||
await updateUserConfigurations({ language: "vi" });
|
||||
await updateUserConfigurations({ language: 'vi' })
|
||||
|
||||
expect(api.patch).toHaveBeenCalledWith("api/configurations?scope=user", {
|
||||
expect(api.patch).toHaveBeenCalledWith('api/configurations?scope=user', {
|
||||
json: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [{ name: "language", value: "vi" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
name: 'core',
|
||||
configurations: [{ name: 'language', value: 'vi' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it("should PATCH configurations with multiple updates", async () => {
|
||||
const mockResponse = { status: 204 };
|
||||
(api.patch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
it('should PATCH configurations with multiple updates', async () => {
|
||||
const mockResponse = { status: 204 }
|
||||
;(api.patch as jest.Mock).mockResolvedValue(mockResponse)
|
||||
|
||||
await updateUserConfigurations({
|
||||
language: "fr",
|
||||
timezone: "Europe/Paris",
|
||||
});
|
||||
language: 'fr',
|
||||
timezone: 'Europe/Paris'
|
||||
})
|
||||
|
||||
expect(api.patch).toHaveBeenCalledWith("api/configurations?scope=user", {
|
||||
expect(api.patch).toHaveBeenCalledWith('api/configurations?scope=user', {
|
||||
json: [
|
||||
{
|
||||
name: "core",
|
||||
name: 'core',
|
||||
configurations: [
|
||||
{ name: "language", value: "fr" },
|
||||
{ name: "datetime", value: { timeZone: "Europe/Paris" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
{ name: 'language', value: 'fr' },
|
||||
{ name: 'datetime', value: { timeZone: 'Europe/Paris' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle empty updates without calling API", async () => {
|
||||
const result = await updateUserConfigurations({});
|
||||
it('should handle empty updates without calling API', async () => {
|
||||
const result = await updateUserConfigurations({})
|
||||
|
||||
expect(api.patch).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ status: 204 });
|
||||
});
|
||||
});
|
||||
expect(api.patch).not.toHaveBeenCalled()
|
||||
expect(result).toEqual({ status: 204 })
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,46 @@
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { fetchWebSocketTicket } from "@/websocket/api/fetchWebSocketTicket";
|
||||
import { api } from '@/utils/apiUtils'
|
||||
import { fetchWebSocketTicket } from '@/websocket/api/fetchWebSocketTicket'
|
||||
|
||||
jest.mock("@/utils/apiUtils");
|
||||
jest.mock('@/utils/apiUtils')
|
||||
|
||||
describe("fetchWebSocketTicket", () => {
|
||||
describe('fetchWebSocketTicket', () => {
|
||||
const mockTicket = {
|
||||
clientAddress: "127.0.0.1",
|
||||
value: "test-ticket-123",
|
||||
generatedOn: "2025-01-12T10:00:00Z",
|
||||
validUntil: "2025-01-12T11:00:00Z",
|
||||
username: "testuser",
|
||||
};
|
||||
clientAddress: '127.0.0.1',
|
||||
value: 'test-ticket-123',
|
||||
generatedOn: '2025-01-12T10:00:00Z',
|
||||
validUntil: '2025-01-12T11:00:00Z',
|
||||
username: 'testuser'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should fetch ticket successfully", async () => {
|
||||
(api.post as jest.Mock).mockResolvedValue({
|
||||
it('should fetch ticket successfully', async () => {
|
||||
;(api.post as jest.Mock).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockTicket,
|
||||
});
|
||||
json: async () => mockTicket
|
||||
})
|
||||
|
||||
const ticket = await fetchWebSocketTicket();
|
||||
const ticket = await fetchWebSocketTicket()
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith("ws/ticket");
|
||||
expect(ticket).toEqual(mockTicket);
|
||||
});
|
||||
expect(api.post).toHaveBeenCalledWith('ws/ticket')
|
||||
expect(ticket).toEqual(mockTicket)
|
||||
})
|
||||
|
||||
it("should throw error when response is not ok", async () => {
|
||||
(api.post as jest.Mock).mockResolvedValue({
|
||||
ok: false,
|
||||
});
|
||||
it('should throw error when response is not ok', async () => {
|
||||
;(api.post as jest.Mock).mockResolvedValue({
|
||||
ok: false
|
||||
})
|
||||
|
||||
await expect(fetchWebSocketTicket()).rejects.toThrow(
|
||||
"Failed to fetch WebSocket ticket"
|
||||
);
|
||||
});
|
||||
'Failed to fetch WebSocket ticket'
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error when network fails", async () => {
|
||||
(api.post as jest.Mock).mockRejectedValue(new Error("Network error"));
|
||||
it('should throw error when network fails', async () => {
|
||||
;(api.post as jest.Mock).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(fetchWebSocketTicket()).rejects.toThrow("Network error");
|
||||
});
|
||||
});
|
||||
await expect(fetchWebSocketTicket()).rejects.toThrow('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,211 +1,210 @@
|
||||
import { fetchWebSocketTicket } from "@/websocket/api/fetchWebSocketTicket";
|
||||
import { createWebSocketConnection } from "@/websocket/connection/createConnection";
|
||||
import { WS_INBOUND_EVENTS } from "@/websocket/protocols";
|
||||
import { waitFor } from "@testing-library/dom";
|
||||
import { setupWebsocket } from "./utils/setupWebsocket";
|
||||
import { fetchWebSocketTicket } from '@/websocket/api/fetchWebSocketTicket'
|
||||
import { createWebSocketConnection } from '@/websocket/connection/createConnection'
|
||||
import { WS_INBOUND_EVENTS } from '@/websocket/protocols'
|
||||
import { waitFor } from '@testing-library/dom'
|
||||
import { setupWebsocket } from './utils/setupWebsocket'
|
||||
|
||||
jest.mock("@/websocket/api/fetchWebSocketTicket");
|
||||
jest.mock('@/websocket/api/fetchWebSocketTicket')
|
||||
|
||||
describe("createWebSocketConnection", () => {
|
||||
let mockWebSocket: jest.Mock;
|
||||
let cleanup: () => void;
|
||||
let webSocketInstances: any[] = [];
|
||||
describe('createWebSocketConnection', () => {
|
||||
let mockWebSocket: jest.Mock
|
||||
let cleanup: () => void
|
||||
let webSocketInstances: any[] = []
|
||||
|
||||
const mockTicket = {
|
||||
value: "test-ticket-123",
|
||||
clientAddress: "127.0.0.1",
|
||||
generatedOn: "2025-01-12T10:00:00Z",
|
||||
validUntil: "2025-01-12T11:00:00Z",
|
||||
username: "testuser",
|
||||
};
|
||||
value: 'test-ticket-123',
|
||||
clientAddress: '127.0.0.1',
|
||||
generatedOn: '2025-01-12T10:00:00Z',
|
||||
validUntil: '2025-01-12T11:00:00Z',
|
||||
username: 'testuser'
|
||||
}
|
||||
|
||||
/** ---------- Helpers ---------- */
|
||||
|
||||
const getWs = () => webSocketInstances[0];
|
||||
const getWs = () => webSocketInstances[0]
|
||||
|
||||
const triggerEvent = (ws: any, event: string, payload?: any) => {
|
||||
ws._listeners[event]?.[0]?.(payload);
|
||||
};
|
||||
ws._listeners[event]?.[0]?.(payload)
|
||||
}
|
||||
|
||||
const createAndOpenConnection = async () => {
|
||||
const mockCallbacks = {
|
||||
onMessage: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
onError: jest.fn(),
|
||||
};
|
||||
const promise = createWebSocketConnection(mockCallbacks);
|
||||
onError: jest.fn()
|
||||
}
|
||||
const promise = createWebSocketConnection(mockCallbacks)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(webSocketInstances.length).toBe(1);
|
||||
});
|
||||
expect(webSocketInstances.length).toBe(1)
|
||||
})
|
||||
|
||||
triggerEvent(getWs(), WS_INBOUND_EVENTS.CONNECTION_OPENED);
|
||||
const socket = await promise;
|
||||
triggerEvent(getWs(), WS_INBOUND_EVENTS.CONNECTION_OPENED)
|
||||
const socket = await promise
|
||||
|
||||
return { socket, ws: getWs(), promise, mockCallbacks };
|
||||
};
|
||||
return { socket, ws: getWs(), promise, mockCallbacks }
|
||||
}
|
||||
|
||||
/** ---------- Setup ---------- */
|
||||
|
||||
beforeEach(() => {
|
||||
({ webSocketInstances, mockWebSocket, cleanup } = setupWebsocket());
|
||||
window.WEBSOCKET_URL = "wss://calendar.example.com";
|
||||
|
||||
(fetchWebSocketTicket as jest.Mock).mockResolvedValue(mockTicket);
|
||||
});
|
||||
;({ webSocketInstances, mockWebSocket, cleanup } = setupWebsocket())
|
||||
window.WEBSOCKET_URL = 'wss://calendar.example.com'
|
||||
;(fetchWebSocketTicket as jest.Mock).mockResolvedValue(mockTicket)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete window.WEBSOCKET_URL;
|
||||
delete window.CALENDAR_BASE_URL;
|
||||
cleanup();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
delete window.WEBSOCKET_URL
|
||||
delete window.CALENDAR_BASE_URL
|
||||
cleanup()
|
||||
})
|
||||
|
||||
/** ---------- Tests ---------- */
|
||||
|
||||
it("throws when WEBSOCKET_URL is not defined", async () => {
|
||||
delete window.WEBSOCKET_URL;
|
||||
it('throws when WEBSOCKET_URL is not defined', async () => {
|
||||
delete window.WEBSOCKET_URL
|
||||
const mockCallbacks = {
|
||||
onMessage: jest.fn(),
|
||||
};
|
||||
onMessage: jest.fn()
|
||||
}
|
||||
|
||||
await expect(createWebSocketConnection(mockCallbacks)).rejects.toThrow(
|
||||
"WEBSOCKET_URL is not defined"
|
||||
);
|
||||
});
|
||||
'WEBSOCKET_URL is not defined'
|
||||
)
|
||||
})
|
||||
|
||||
it("fetches WebSocket ticket", async () => {
|
||||
await createAndOpenConnection();
|
||||
expect(fetchWebSocketTicket).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('fetches WebSocket ticket', async () => {
|
||||
await createAndOpenConnection()
|
||||
expect(fetchWebSocketTicket).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("creates WebSocket with correct URL and ticket", async () => {
|
||||
await createAndOpenConnection();
|
||||
it('creates WebSocket with correct URL and ticket', async () => {
|
||||
await createAndOpenConnection()
|
||||
|
||||
expect(mockWebSocket).toHaveBeenCalledWith(
|
||||
"wss://calendar.example.com/ws?ticket=test-ticket-123"
|
||||
);
|
||||
});
|
||||
'wss://calendar.example.com/ws?ticket=test-ticket-123'
|
||||
)
|
||||
})
|
||||
|
||||
it("creates WebSocket with correct URL and ticket without the WEBSOCKET_URL", async () => {
|
||||
delete window.WEBSOCKET_URL;
|
||||
window.CALENDAR_BASE_URL = "https://calendar.example.com";
|
||||
await createAndOpenConnection();
|
||||
it('creates WebSocket with correct URL and ticket without the WEBSOCKET_URL', async () => {
|
||||
delete window.WEBSOCKET_URL
|
||||
window.CALENDAR_BASE_URL = 'https://calendar.example.com'
|
||||
await createAndOpenConnection()
|
||||
|
||||
expect(mockWebSocket).toHaveBeenCalledWith(
|
||||
"wss://calendar.example.com/ws?ticket=test-ticket-123"
|
||||
);
|
||||
});
|
||||
'wss://calendar.example.com/ws?ticket=test-ticket-123'
|
||||
)
|
||||
})
|
||||
|
||||
it("resolves with socket when connection opens", async () => {
|
||||
const { socket, ws } = await createAndOpenConnection();
|
||||
expect(socket).toBe(ws);
|
||||
});
|
||||
it('resolves with socket when connection opens', async () => {
|
||||
const { socket, ws } = await createAndOpenConnection()
|
||||
expect(socket).toBe(ws)
|
||||
})
|
||||
|
||||
it("rejects when connection fails", async () => {
|
||||
it('rejects when connection fails', async () => {
|
||||
const mockCallbacks = {
|
||||
onMessage: jest.fn(),
|
||||
};
|
||||
const promise = createWebSocketConnection(mockCallbacks);
|
||||
onMessage: jest.fn()
|
||||
}
|
||||
const promise = createWebSocketConnection(mockCallbacks)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(webSocketInstances.length).toBe(1);
|
||||
});
|
||||
expect(webSocketInstances.length).toBe(1)
|
||||
})
|
||||
|
||||
triggerEvent(getWs(), WS_INBOUND_EVENTS.ERROR, new Event("error"));
|
||||
triggerEvent(getWs(), WS_INBOUND_EVENTS.ERROR, new Event('error'))
|
||||
|
||||
await expect(promise).rejects.toThrow("WebSocket connection failed");
|
||||
});
|
||||
await expect(promise).rejects.toThrow('WebSocket connection failed')
|
||||
})
|
||||
|
||||
it("attaches message event listener", async () => {
|
||||
const { ws } = await createAndOpenConnection();
|
||||
it('attaches message event listener', async () => {
|
||||
const { ws } = await createAndOpenConnection()
|
||||
|
||||
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE]).toBeDefined();
|
||||
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE].length).toBeGreaterThan(0);
|
||||
});
|
||||
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE]).toBeDefined()
|
||||
expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE].length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("attaches close event listener", async () => {
|
||||
const { ws } = await createAndOpenConnection();
|
||||
expect(ws._listeners[WS_INBOUND_EVENTS.CONNECTION_CLOSED]).toBeDefined();
|
||||
});
|
||||
it('attaches close event listener', async () => {
|
||||
const { ws } = await createAndOpenConnection()
|
||||
expect(ws._listeners[WS_INBOUND_EVENTS.CONNECTION_CLOSED]).toBeDefined()
|
||||
})
|
||||
|
||||
it("handles invalid JSON messages", async () => {
|
||||
const errorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
it('handles invalid JSON messages', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
|
||||
const { ws } = await createAndOpenConnection();
|
||||
const { ws } = await createAndOpenConnection()
|
||||
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: "invalid json" });
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: 'invalid json' })
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Failed to parse WebSocket message:",
|
||||
'Failed to parse WebSocket message:',
|
||||
expect.any(Error)
|
||||
);
|
||||
)
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
errorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("rejects on timeout", async () => {
|
||||
jest.useFakeTimers();
|
||||
it('rejects on timeout', async () => {
|
||||
jest.useFakeTimers()
|
||||
const mockCallbacks = {
|
||||
onMessage: jest.fn(),
|
||||
};
|
||||
const promise = createWebSocketConnection(mockCallbacks);
|
||||
onMessage: jest.fn()
|
||||
}
|
||||
const promise = createWebSocketConnection(mockCallbacks)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(webSocketInstances.length).toBe(1);
|
||||
});
|
||||
expect(webSocketInstances.length).toBe(1)
|
||||
})
|
||||
|
||||
jest.advanceTimersByTime(10000);
|
||||
jest.advanceTimersByTime(10000)
|
||||
|
||||
await expect(promise).rejects.toThrow("WebSocket connection timed out");
|
||||
await expect(promise).rejects.toThrow('WebSocket connection timed out')
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it("calls onMessage callback when message received", async () => {
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection();
|
||||
it('calls onMessage callback when message received', async () => {
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection()
|
||||
|
||||
const testMessage = { type: "test", payload: "data" };
|
||||
const testMessage = { type: 'test', payload: 'data' }
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, {
|
||||
data: JSON.stringify(testMessage),
|
||||
});
|
||||
data: JSON.stringify(testMessage)
|
||||
})
|
||||
|
||||
expect(mockCallbacks.onMessage).toHaveBeenCalledWith(testMessage);
|
||||
});
|
||||
expect(mockCallbacks.onMessage).toHaveBeenCalledWith(testMessage)
|
||||
})
|
||||
|
||||
it("does not call onMessage when JSON parsing fails", async () => {
|
||||
const errorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection();
|
||||
it('does not call onMessage when JSON parsing fails', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection()
|
||||
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: "invalid json" });
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: 'invalid json' })
|
||||
|
||||
expect(mockCallbacks.onMessage).not.toHaveBeenCalled();
|
||||
expect(mockCallbacks.onMessage).not.toHaveBeenCalled()
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Failed to parse WebSocket message:",
|
||||
'Failed to parse WebSocket message:',
|
||||
expect.any(Error)
|
||||
);
|
||||
)
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
errorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("calls onClose callback when connection closes", async () => {
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection();
|
||||
it('calls onClose callback when connection closes', async () => {
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection()
|
||||
|
||||
const closeEvent = new CloseEvent("close", {
|
||||
const closeEvent = new CloseEvent('close', {
|
||||
code: 1000,
|
||||
reason: "Normal closure",
|
||||
});
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeEvent);
|
||||
reason: 'Normal closure'
|
||||
})
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeEvent)
|
||||
|
||||
expect(mockCallbacks.onClose).toHaveBeenCalledWith(closeEvent);
|
||||
});
|
||||
expect(mockCallbacks.onClose).toHaveBeenCalledWith(closeEvent)
|
||||
})
|
||||
|
||||
it("calls onError callback when error occurs", async () => {
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection();
|
||||
it('calls onError callback when error occurs', async () => {
|
||||
const { ws, mockCallbacks } = await createAndOpenConnection()
|
||||
|
||||
const errorEvent = new Event("error");
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.ERROR, errorEvent);
|
||||
const errorEvent = new Event('error')
|
||||
triggerEvent(ws, WS_INBOUND_EVENTS.ERROR, errorEvent)
|
||||
|
||||
expect(mockCallbacks.onError).toHaveBeenCalledWith(errorEvent);
|
||||
});
|
||||
});
|
||||
expect(mockCallbacks.onError).toHaveBeenCalledWith(errorEvent)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { getRetryDelay } from "@/utils/getRetryDelay";
|
||||
import { getRetryDelay } from '@/utils/getRetryDelay'
|
||||
import {
|
||||
MAX_RECONNECT_ATTEMPTS,
|
||||
RECONNECT_CONFIG,
|
||||
useWebSocketReconnect,
|
||||
} from "@/websocket/connection/lifecycle/useWebSocketReconnect";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { MutableRefObject } from "react";
|
||||
useWebSocketReconnect
|
||||
} from '@/websocket/connection/lifecycle/useWebSocketReconnect'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { MutableRefObject } from 'react'
|
||||
|
||||
// Mock the retry delay utility
|
||||
jest.mock("@/utils/getRetryDelay");
|
||||
jest.mock('@/utils/getRetryDelay')
|
||||
const mockGetRetryDelay = getRetryDelay as jest.MockedFunction<
|
||||
typeof getRetryDelay
|
||||
>;
|
||||
>
|
||||
|
||||
describe("useWebSocketReconnect", () => {
|
||||
let reconnectTimeoutRef: MutableRefObject<NodeJS.Timeout | null>;
|
||||
let isAuthenticatedRef: MutableRefObject<boolean>;
|
||||
let reconnectAttemptsRef: MutableRefObject<number>;
|
||||
let setShouldConnect: jest.Mock;
|
||||
describe('useWebSocketReconnect', () => {
|
||||
let reconnectTimeoutRef: MutableRefObject<NodeJS.Timeout | null>
|
||||
let isAuthenticatedRef: MutableRefObject<boolean>
|
||||
let reconnectAttemptsRef: MutableRefObject<number>
|
||||
let setShouldConnect: jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
reconnectTimeoutRef = { current: null };
|
||||
isAuthenticatedRef = { current: true };
|
||||
reconnectAttemptsRef = { current: 0 };
|
||||
setShouldConnect = jest.fn();
|
||||
mockGetRetryDelay.mockReturnValue(1000); // Default 1 second delay
|
||||
});
|
||||
jest.useFakeTimers()
|
||||
reconnectTimeoutRef = { current: null }
|
||||
isAuthenticatedRef = { current: true }
|
||||
reconnectAttemptsRef = { current: 0 }
|
||||
setShouldConnect = jest.fn()
|
||||
mockGetRetryDelay.mockReturnValue(1000) // Default 1 second delay
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
jest.runOnlyPendingTimers()
|
||||
jest.useRealTimers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("scheduleReconnect", () => {
|
||||
it("should schedule a reconnection with correct delay", () => {
|
||||
describe('scheduleReconnect', () => {
|
||||
it('should schedule a reconnection with correct delay', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -43,26 +43,26 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
})
|
||||
|
||||
expect(mockGetRetryDelay).toHaveBeenCalledWith(0, RECONNECT_CONFIG);
|
||||
expect(reconnectTimeoutRef.current).not.toBeNull();
|
||||
expect(setShouldConnect).not.toHaveBeenCalled();
|
||||
expect(mockGetRetryDelay).toHaveBeenCalledWith(0, RECONNECT_CONFIG)
|
||||
expect(reconnectTimeoutRef.current).not.toBeNull()
|
||||
expect(setShouldConnect).not.toHaveBeenCalled()
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
expect(setShouldConnect).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(reconnectAttemptsRef.current).toBe(1);
|
||||
});
|
||||
expect(setShouldConnect).toHaveBeenCalledWith(expect.any(Function))
|
||||
expect(reconnectAttemptsRef.current).toBe(1)
|
||||
})
|
||||
|
||||
it("should not schedule reconnection if not authenticated", () => {
|
||||
isAuthenticatedRef = { current: false };
|
||||
it('should not schedule reconnection if not authenticated', () => {
|
||||
isAuthenticatedRef = { current: false }
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -70,19 +70,19 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
})
|
||||
|
||||
expect(reconnectTimeoutRef.current).toBeNull();
|
||||
expect(mockGetRetryDelay).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(reconnectTimeoutRef.current).toBeNull()
|
||||
expect(mockGetRetryDelay).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should stop after MAX_RECONNECT_ATTEMPTS", () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
reconnectAttemptsRef.current = MAX_RECONNECT_ATTEMPTS;
|
||||
it('should stop after MAX_RECONNECT_ATTEMPTS', () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
reconnectAttemptsRef.current = MAX_RECONNECT_ATTEMPTS
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
@@ -91,23 +91,23 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
})
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Max WebSocket reconnection attempts (${MAX_RECONNECT_ATTEMPTS})`
|
||||
)
|
||||
);
|
||||
expect(reconnectTimeoutRef.current).toBeNull();
|
||||
)
|
||||
expect(reconnectTimeoutRef.current).toBeNull()
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should increment attempt counter on each reconnection", () => {
|
||||
it('should increment attempt counter on each reconnection', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -115,26 +115,26 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
expect(reconnectAttemptsRef.current).toBe(0);
|
||||
expect(reconnectAttemptsRef.current).toBe(0)
|
||||
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
expect(reconnectAttemptsRef.current).toBe(1);
|
||||
expect(reconnectAttemptsRef.current).toBe(1)
|
||||
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
expect(reconnectAttemptsRef.current).toBe(2);
|
||||
});
|
||||
expect(reconnectAttemptsRef.current).toBe(2)
|
||||
})
|
||||
|
||||
it("should toggle setShouldConnect correctly", () => {
|
||||
it('should toggle setShouldConnect correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -142,24 +142,24 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(1);
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Test the toggle function
|
||||
const toggleFn = setShouldConnect.mock.calls[0][0];
|
||||
expect(toggleFn(false)).toBe(true);
|
||||
expect(toggleFn(true)).toBe(false);
|
||||
});
|
||||
});
|
||||
const toggleFn = setShouldConnect.mock.calls[0][0]
|
||||
expect(toggleFn(false)).toBe(true)
|
||||
expect(toggleFn(true)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("clearReconnectTimeout", () => {
|
||||
it("should clear pending timeout", () => {
|
||||
describe('clearReconnectTimeout', () => {
|
||||
it('should clear pending timeout', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -167,29 +167,29 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
})
|
||||
|
||||
expect(reconnectTimeoutRef.current).not.toBeNull();
|
||||
expect(reconnectTimeoutRef.current).not.toBeNull()
|
||||
|
||||
act(() => {
|
||||
result.current.clearReconnectTimeout();
|
||||
});
|
||||
result.current.clearReconnectTimeout()
|
||||
})
|
||||
|
||||
expect(reconnectTimeoutRef.current).toBeNull();
|
||||
expect(reconnectTimeoutRef.current).toBeNull()
|
||||
|
||||
// Timeout should not fire
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
expect(setShouldConnect).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(setShouldConnect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle multiple clears gracefully", () => {
|
||||
it('should handle multiple clears gracefully', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -197,18 +197,18 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.clearReconnectTimeout();
|
||||
result.current.clearReconnectTimeout();
|
||||
result.current.clearReconnectTimeout();
|
||||
});
|
||||
result.current.clearReconnectTimeout()
|
||||
result.current.clearReconnectTimeout()
|
||||
result.current.clearReconnectTimeout()
|
||||
})
|
||||
|
||||
expect(reconnectTimeoutRef.current).toBeNull();
|
||||
});
|
||||
expect(reconnectTimeoutRef.current).toBeNull()
|
||||
})
|
||||
|
||||
it("should clear timeout before scheduling new one", () => {
|
||||
it('should clear timeout before scheduling new one', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -216,35 +216,35 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
// Schedule first reconnection
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
})
|
||||
|
||||
const firstTimeout = reconnectTimeoutRef.current;
|
||||
expect(firstTimeout).not.toBeNull();
|
||||
const firstTimeout = reconnectTimeoutRef.current
|
||||
expect(firstTimeout).not.toBeNull()
|
||||
|
||||
// Schedule second reconnection (should clear first)
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
})
|
||||
|
||||
const secondTimeout = reconnectTimeoutRef.current;
|
||||
expect(secondTimeout).not.toBeNull();
|
||||
expect(secondTimeout).not.toBe(firstTimeout);
|
||||
const secondTimeout = reconnectTimeoutRef.current
|
||||
expect(secondTimeout).not.toBeNull()
|
||||
expect(secondTimeout).not.toBe(firstTimeout)
|
||||
|
||||
// Only second timeout should fire
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
it("should stop reconnecting after MAX_RECONNECT_ATTEMPTS (10)", () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
it('should stop reconnecting after MAX_RECONNECT_ATTEMPTS (10)', () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
@@ -252,32 +252,32 @@ describe("useWebSocketReconnect", () => {
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
// Simulate 10 reconnection attempts
|
||||
for (let i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) {
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
result.current.scheduleReconnect()
|
||||
jest.advanceTimersByTime(
|
||||
mockGetRetryDelay.mock.results[i]?.value || 1000
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
expect(reconnectAttemptsRef.current).toBe(MAX_RECONNECT_ATTEMPTS);
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS);
|
||||
expect(reconnectAttemptsRef.current).toBe(MAX_RECONNECT_ATTEMPTS)
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS)
|
||||
|
||||
// Try to schedule one more reconnection - should fail
|
||||
act(() => {
|
||||
result.current.scheduleReconnect();
|
||||
});
|
||||
result.current.scheduleReconnect()
|
||||
})
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
`Max WebSocket reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`
|
||||
);
|
||||
expect(reconnectTimeoutRef.current).toBeNull();
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS); // Should not increment
|
||||
)
|
||||
expect(reconnectTimeoutRef.current).toBeNull()
|
||||
expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS) // Should not increment
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,135 +1,135 @@
|
||||
import type { AppDispatch, RootState } from "@/app/store";
|
||||
import { store } from "@/app/store";
|
||||
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services";
|
||||
import { getDisplayedCalendarRange } from "@/utils";
|
||||
import { updateCalendars } from "@/websocket/messaging/updateCalendars";
|
||||
import type { AppDispatch, RootState } from '@/app/store'
|
||||
import { store } from '@/app/store'
|
||||
import { refreshCalendarWithSyncToken } from '@/features/Calendars/services'
|
||||
import { getDisplayedCalendarRange } from '@/utils'
|
||||
import { updateCalendars } from '@/websocket/messaging/updateCalendars'
|
||||
|
||||
jest.mock("@/features/Calendars/services", () => ({
|
||||
refreshCalendarWithSyncToken: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/features/Calendars/services', () => ({
|
||||
refreshCalendarWithSyncToken: jest.fn()
|
||||
}))
|
||||
|
||||
jest.mock("@/utils", () => ({
|
||||
jest.mock('@/utils', () => ({
|
||||
getDisplayedCalendarRange: jest.fn(),
|
||||
findCalendarById: jest.requireActual("@/utils").findCalendarById,
|
||||
}));
|
||||
findCalendarById: jest.requireActual('@/utils').findCalendarById
|
||||
}))
|
||||
|
||||
jest.mock("@/app/store", () => ({
|
||||
jest.mock('@/app/store', () => ({
|
||||
store: {
|
||||
getState: jest.fn(),
|
||||
},
|
||||
}));
|
||||
getState: jest.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
jest.useFakeTimers();
|
||||
const mockDispatch = jest.fn();
|
||||
jest.useFakeTimers()
|
||||
const mockDispatch = jest.fn()
|
||||
const mockRange = {
|
||||
start: new Date("2025-01-15T10:00:00Z"),
|
||||
end: new Date("2025-01-16T10:00:00Z"),
|
||||
};
|
||||
start: new Date('2025-01-15T10:00:00Z'),
|
||||
end: new Date('2025-01-16T10:00:00Z')
|
||||
}
|
||||
const mockState = {
|
||||
calendars: {
|
||||
list: {
|
||||
"cal1/entry1": { id: "cal1/entry1", name: "Calendar 1", syncToken: 1 },
|
||||
"cal2/entry2": { id: "cal2/entry2", name: "Calendar 2", syncToken: 1 },
|
||||
"cal/A": { id: "cal/A", name: "Cal A", syncToken: 1 },
|
||||
"cal/B": { id: "cal/B", name: "Cal B", syncToken: 1 },
|
||||
"cal/C": { id: "cal/C", name: "Cal C", syncToken: 1 },
|
||||
'cal1/entry1': { id: 'cal1/entry1', name: 'Calendar 1', syncToken: 1 },
|
||||
'cal2/entry2': { id: 'cal2/entry2', name: 'Calendar 2', syncToken: 1 },
|
||||
'cal/A': { id: 'cal/A', name: 'Cal A', syncToken: 1 },
|
||||
'cal/B': { id: 'cal/B', name: 'Cal B', syncToken: 1 },
|
||||
'cal/C': { id: 'cal/C', name: 'Cal C', syncToken: 1 }
|
||||
},
|
||||
templist: {},
|
||||
},
|
||||
} as unknown as RootState;
|
||||
templist: {}
|
||||
}
|
||||
} as unknown as RootState
|
||||
const mockAccumulators: {
|
||||
calendarsToRefresh: Map<string, any>;
|
||||
calendarsToHide: Set<string>;
|
||||
debouncedUpdateFn?: (dispatch: AppDispatch) => void;
|
||||
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>;
|
||||
currentDebouncePeriod?: number;
|
||||
calendarsToRefresh: Map<string, any>
|
||||
calendarsToHide: Set<string>
|
||||
debouncedUpdateFn?: (dispatch: AppDispatch) => void
|
||||
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>
|
||||
currentDebouncePeriod?: number
|
||||
} = {
|
||||
calendarsToRefresh: new Map<string, any>(),
|
||||
calendarsToHide: new Set(),
|
||||
shouldRefreshCalendarListRef: { current: false },
|
||||
currentDebouncePeriod: 0,
|
||||
debouncedUpdateFn: undefined,
|
||||
};
|
||||
debouncedUpdateFn: undefined
|
||||
}
|
||||
|
||||
describe("websocket messages storm", () => {
|
||||
describe('websocket messages storm', () => {
|
||||
beforeEach(() => {
|
||||
(refreshCalendarWithSyncToken as unknown as jest.Mock).mockClear();
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllTimers();
|
||||
jest.resetModules();
|
||||
(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange);
|
||||
(store.getState as jest.Mock).mockReturnValue(mockState);
|
||||
window.WS_DEBOUNCE_PERIOD_MS = 500;
|
||||
mockAccumulators.calendarsToRefresh = new Map<string, any>();
|
||||
mockAccumulators.calendarsToHide = new Set();
|
||||
mockAccumulators.shouldRefreshCalendarListRef.current = false;
|
||||
mockAccumulators.currentDebouncePeriod = 0;
|
||||
;(refreshCalendarWithSyncToken as unknown as jest.Mock).mockClear()
|
||||
jest.clearAllMocks()
|
||||
jest.clearAllTimers()
|
||||
jest.resetModules()
|
||||
;(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange)
|
||||
;(store.getState as jest.Mock).mockReturnValue(mockState)
|
||||
window.WS_DEBOUNCE_PERIOD_MS = 500
|
||||
mockAccumulators.calendarsToRefresh = new Map<string, any>()
|
||||
mockAccumulators.calendarsToHide = new Set()
|
||||
mockAccumulators.shouldRefreshCalendarListRef.current = false
|
||||
mockAccumulators.currentDebouncePeriod = 0
|
||||
|
||||
mockAccumulators.debouncedUpdateFn = undefined;
|
||||
});
|
||||
it("debounces calendar updates during message storm", () => {
|
||||
mockAccumulators.debouncedUpdateFn = undefined
|
||||
})
|
||||
it('debounces calendar updates during message storm', () => {
|
||||
const mockMessage = {
|
||||
"/calendars/cal1/entry1": {
|
||||
syncToken: "ldsk",
|
||||
},
|
||||
};
|
||||
'/calendars/cal1/entry1': {
|
||||
syncToken: 'ldsk'
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
updateCalendars(mockMessage, mockDispatch, mockAccumulators);
|
||||
updateCalendars(mockMessage, mockDispatch, mockAccumulators)
|
||||
}
|
||||
|
||||
// Dispatch called once because of leading edge
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Trailing edge
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(500)
|
||||
|
||||
// only one call for the last message + leading edge message
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("debounces calendar updates during message storm with multiple updates", () => {
|
||||
it('debounces calendar updates during message storm with multiple updates', () => {
|
||||
// Send a storm with mixed messages
|
||||
for (let i = 0; i < 50; i++) {
|
||||
if (i % 3 === 0)
|
||||
updateCalendars(
|
||||
{ "/calendars/cal/A": { syncToken: "ldskfjsld" + i } },
|
||||
{ '/calendars/cal/A': { syncToken: 'ldskfjsld' + i } },
|
||||
mockDispatch,
|
||||
mockAccumulators
|
||||
);
|
||||
)
|
||||
else if (i % 3 === 1)
|
||||
updateCalendars(
|
||||
{ "/calendars/cal/B": { syncToken: "ldskfjsld" + i } },
|
||||
{ '/calendars/cal/B': { syncToken: 'ldskfjsld' + i } },
|
||||
mockDispatch,
|
||||
mockAccumulators
|
||||
);
|
||||
)
|
||||
else
|
||||
updateCalendars(
|
||||
{ "/calendars/cal/C": { syncToken: "ldskfjsld" + i } },
|
||||
{ '/calendars/cal/C': { syncToken: 'ldskfjsld' + i } },
|
||||
mockDispatch,
|
||||
mockAccumulators
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Dispatch called once because of leading edge
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Trailing edge
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(500)
|
||||
|
||||
// Trailing edge updates once per calendar + the original leading edge
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it("executes immediately when debounce is disabled", () => {
|
||||
window.WS_DEBOUNCE_PERIOD_MS = 0;
|
||||
it('executes immediately when debounce is disabled', () => {
|
||||
window.WS_DEBOUNCE_PERIOD_MS = 0
|
||||
|
||||
updateCalendars(
|
||||
{ "/calendars/cal1/entry1": { syncToken: "abc" } },
|
||||
{ '/calendars/cal1/entry1': { syncToken: 'abc' } },
|
||||
mockDispatch,
|
||||
mockAccumulators
|
||||
);
|
||||
)
|
||||
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function setupWebsocket() {
|
||||
const originalWebSocket = global.WebSocket;
|
||||
const webSocketInstances: any[] = [];
|
||||
const originalWebSocket = global.WebSocket
|
||||
const webSocketInstances: any[] = []
|
||||
const mockWebSocket = jest.fn().mockImplementation((url: string) => {
|
||||
const ws = {
|
||||
url,
|
||||
@@ -9,27 +9,27 @@ export function setupWebsocket() {
|
||||
removeEventListener: jest.fn(),
|
||||
send: jest.fn(),
|
||||
close: jest.fn(),
|
||||
_listeners: {} as Record<string, Function[]>,
|
||||
};
|
||||
_listeners: {} as Record<string, Function[]>
|
||||
}
|
||||
|
||||
ws.addEventListener.mockImplementation((event, handler) => {
|
||||
ws._listeners[event] ??= [];
|
||||
ws._listeners[event].push(handler);
|
||||
});
|
||||
ws._listeners[event] ??= []
|
||||
ws._listeners[event].push(handler)
|
||||
})
|
||||
|
||||
ws.removeEventListener.mockImplementation((event, handler) => {
|
||||
ws._listeners[event] =
|
||||
ws._listeners[event]?.filter((h) => h !== handler) ?? [];
|
||||
});
|
||||
ws._listeners[event]?.filter(h => h !== handler) ?? []
|
||||
})
|
||||
|
||||
webSocketInstances.push(ws);
|
||||
return ws;
|
||||
});
|
||||
webSocketInstances.push(ws)
|
||||
return ws
|
||||
})
|
||||
|
||||
global.WebSocket = mockWebSocket as any;
|
||||
global.WebSocket = mockWebSocket as any
|
||||
const cleanup = () => {
|
||||
global.WebSocket = originalWebSocket;
|
||||
webSocketInstances.length = 0;
|
||||
};
|
||||
return { webSocketInstances, mockWebSocket, cleanup };
|
||||
global.WebSocket = originalWebSocket
|
||||
webSocketInstances.length = 0
|
||||
}
|
||||
return { webSocketInstances, mockWebSocket, cleanup }
|
||||
}
|
||||
|
||||
@@ -1,147 +1,145 @@
|
||||
import { AppDispatch, RootState, store } from "@/app/store";
|
||||
import { refreshCalendarWithSyncToken } from "@/features/Calendars/services/refreshCalendar";
|
||||
import { getDisplayedCalendarRange } from "@/utils/CalendarRangeManager";
|
||||
import { updateCalendars } from "@/websocket/messaging/updateCalendars";
|
||||
import { WS_INBOUND_EVENTS } from "@/websocket/protocols";
|
||||
import { waitFor } from "@testing-library/dom";
|
||||
import { AppDispatch, RootState, store } from '@/app/store'
|
||||
import { refreshCalendarWithSyncToken } from '@/features/Calendars/services/refreshCalendar'
|
||||
import { getDisplayedCalendarRange } from '@/utils/CalendarRangeManager'
|
||||
import { updateCalendars } from '@/websocket/messaging/updateCalendars'
|
||||
import { WS_INBOUND_EVENTS } from '@/websocket/protocols'
|
||||
import { waitFor } from '@testing-library/dom'
|
||||
|
||||
jest.mock("@/features/Calendars/services/refreshCalendar");
|
||||
jest.mock("@/utils/CalendarRangeManager");
|
||||
jest.mock("@/app/store", () => ({
|
||||
jest.mock('@/features/Calendars/services/refreshCalendar')
|
||||
jest.mock('@/utils/CalendarRangeManager')
|
||||
jest.mock('@/app/store', () => ({
|
||||
store: {
|
||||
getState: jest.fn(),
|
||||
},
|
||||
}));
|
||||
getState: jest.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe("updateCalendars", () => {
|
||||
let mockDispatch: jest.Mock;
|
||||
describe('updateCalendars', () => {
|
||||
let mockDispatch: jest.Mock
|
||||
const mockRange = {
|
||||
start: new Date("2025-01-15T10:00:00Z"),
|
||||
end: new Date("2025-01-16T10:00:00Z"),
|
||||
};
|
||||
start: new Date('2025-01-15T10:00:00Z'),
|
||||
end: new Date('2025-01-16T10:00:00Z')
|
||||
}
|
||||
|
||||
const mockState = {
|
||||
calendars: {
|
||||
list: {
|
||||
"cal1/entry1": { id: "cal1/entry1", name: "Calendar 1", syncToken: 1 },
|
||||
"cal2/entry2": { id: "cal2/entry2", name: "Calendar 2", syncToken: 1 },
|
||||
'cal1/entry1': { id: 'cal1/entry1', name: 'Calendar 1', syncToken: 1 },
|
||||
'cal2/entry2': { id: 'cal2/entry2', name: 'Calendar 2', syncToken: 1 }
|
||||
},
|
||||
templist: {},
|
||||
},
|
||||
} as unknown as RootState;
|
||||
templist: {}
|
||||
}
|
||||
} as unknown as RootState
|
||||
const mockAccumulators: {
|
||||
calendarsToRefresh: Map<string, any>;
|
||||
calendarsToHide: Set<string>;
|
||||
debouncedUpdateFn?: (dispatch: AppDispatch) => void;
|
||||
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>;
|
||||
currentDebouncePeriod?: number;
|
||||
calendarsToRefresh: Map<string, any>
|
||||
calendarsToHide: Set<string>
|
||||
debouncedUpdateFn?: (dispatch: AppDispatch) => void
|
||||
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>
|
||||
currentDebouncePeriod?: number
|
||||
} = {
|
||||
calendarsToRefresh: new Map<string, any>(),
|
||||
calendarsToHide: new Set(),
|
||||
shouldRefreshCalendarListRef: { current: false },
|
||||
currentDebouncePeriod: 0,
|
||||
debouncedUpdateFn: jest.fn(),
|
||||
};
|
||||
debouncedUpdateFn: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDispatch = jest.fn();
|
||||
(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange);
|
||||
(store.getState as jest.Mock).mockReturnValue(mockState);
|
||||
mockAccumulators.calendarsToRefresh = new Map<string, any>();
|
||||
mockAccumulators.calendarsToHide = new Set();
|
||||
mockAccumulators.currentDebouncePeriod = 0;
|
||||
mockAccumulators.shouldRefreshCalendarListRef.current = false;
|
||||
mockAccumulators.debouncedUpdateFn = jest.fn();
|
||||
});
|
||||
jest.clearAllMocks()
|
||||
mockDispatch = jest.fn()
|
||||
;(getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange)
|
||||
;(store.getState as jest.Mock).mockReturnValue(mockState)
|
||||
mockAccumulators.calendarsToRefresh = new Map<string, any>()
|
||||
mockAccumulators.calendarsToHide = new Set()
|
||||
mockAccumulators.currentDebouncePeriod = 0
|
||||
mockAccumulators.shouldRefreshCalendarListRef.current = false
|
||||
mockAccumulators.debouncedUpdateFn = jest.fn()
|
||||
})
|
||||
|
||||
it("should not dispatch for non-object messages", () => {
|
||||
updateCalendars(null, mockDispatch, mockAccumulators);
|
||||
updateCalendars("string", mockDispatch, mockAccumulators);
|
||||
updateCalendars(123, mockDispatch, mockAccumulators);
|
||||
it('should not dispatch for non-object messages', () => {
|
||||
updateCalendars(null, mockDispatch, mockAccumulators)
|
||||
updateCalendars('string', mockDispatch, mockAccumulators)
|
||||
updateCalendars(123, mockDispatch, mockAccumulators)
|
||||
|
||||
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should dispatch for registered calendars", async () => {
|
||||
it('should dispatch for registered calendars', async () => {
|
||||
const message = {
|
||||
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: [
|
||||
"/calendars/cal1/entry1",
|
||||
"/calendars/cal2/entry2",
|
||||
],
|
||||
};
|
||||
'/calendars/cal1/entry1',
|
||||
'/calendars/cal2/entry2'
|
||||
]
|
||||
}
|
||||
|
||||
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||
updateCalendars(message, mockDispatch, mockAccumulators)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledTimes(2)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should dispatch for calendar path updates", async () => {
|
||||
it('should dispatch for calendar path updates', async () => {
|
||||
const message = {
|
||||
"/calendars/cal1/entry1": { updated: true },
|
||||
};
|
||||
'/calendars/cal1/entry1': { updated: true }
|
||||
}
|
||||
|
||||
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||
await waitFor(() =>
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalled()
|
||||
);
|
||||
updateCalendars(message, mockDispatch, mockAccumulators)
|
||||
await waitFor(() => expect(refreshCalendarWithSyncToken).toHaveBeenCalled())
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
|
||||
calendar: mockState.calendars.list["cal1/entry1"],
|
||||
calendar: mockState.calendars.list['cal1/entry1'],
|
||||
calType: undefined,
|
||||
calendarRange: mockRange,
|
||||
});
|
||||
});
|
||||
calendarRange: mockRange
|
||||
})
|
||||
})
|
||||
|
||||
it("should use displayed calendar range", async () => {
|
||||
it('should use displayed calendar range', async () => {
|
||||
const message = {
|
||||
"/calendars/cal1/entry1": {},
|
||||
};
|
||||
'/calendars/cal1/entry1': {}
|
||||
}
|
||||
|
||||
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||
await waitFor(() => expect(getDisplayedCalendarRange).toHaveBeenCalled());
|
||||
});
|
||||
updateCalendars(message, mockDispatch, mockAccumulators)
|
||||
await waitFor(() => expect(getDisplayedCalendarRange).toHaveBeenCalled())
|
||||
})
|
||||
|
||||
it("should handle temp calendars", async () => {
|
||||
it('should handle temp calendars', async () => {
|
||||
const stateWithTemp = {
|
||||
calendars: {
|
||||
list: {},
|
||||
templist: {
|
||||
"temp1/entry1": {
|
||||
id: "temp1/entry1",
|
||||
name: "Temp Calendar",
|
||||
syncToken: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
'temp1/entry1': {
|
||||
id: 'temp1/entry1',
|
||||
name: 'Temp Calendar',
|
||||
syncToken: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(store.getState as jest.Mock).mockReturnValue(stateWithTemp);
|
||||
;(store.getState as jest.Mock).mockReturnValue(stateWithTemp)
|
||||
|
||||
const message = {
|
||||
"/calendars/temp1/entry1": {},
|
||||
};
|
||||
'/calendars/temp1/entry1': {}
|
||||
}
|
||||
|
||||
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||
updateCalendars(message, mockDispatch, mockAccumulators)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(refreshCalendarWithSyncToken).toHaveBeenCalledWith({
|
||||
calendar: stateWithTemp.calendars.templist["temp1/entry1"],
|
||||
calType: "temp",
|
||||
calendarRange: mockRange,
|
||||
calendar: stateWithTemp.calendars.templist['temp1/entry1'],
|
||||
calType: 'temp',
|
||||
calendarRange: mockRange
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle invalid calendar paths gracefully", () => {
|
||||
it('should handle invalid calendar paths gracefully', () => {
|
||||
const message = {
|
||||
"/invalid/path": {},
|
||||
"not-a-path": {},
|
||||
};
|
||||
'/invalid/path': {},
|
||||
'not-a-path': {}
|
||||
}
|
||||
|
||||
updateCalendars(message, mockDispatch, mockAccumulators);
|
||||
updateCalendars(message, mockDispatch, mockAccumulators)
|
||||
|
||||
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(refreshCalendarWithSyncToken).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
import { parseMessage } from "@/websocket/messaging/parseMessage";
|
||||
import { WS_INBOUND_EVENTS } from "@/websocket/protocols";
|
||||
import { parseMessage } from '@/websocket/messaging/parseMessage'
|
||||
import { WS_INBOUND_EVENTS } from '@/websocket/protocols'
|
||||
|
||||
describe("parseMessage", () => {
|
||||
it("should return empty set for non-object messages", () => {
|
||||
const result1 = parseMessage(null);
|
||||
const result2 = parseMessage("string");
|
||||
const result3 = parseMessage(123);
|
||||
describe('parseMessage', () => {
|
||||
it('should return empty set for non-object messages', () => {
|
||||
const result1 = parseMessage(null)
|
||||
const result2 = parseMessage('string')
|
||||
const result3 = parseMessage(123)
|
||||
|
||||
expect(result1.calendarsToRefresh).toEqual(new Set<string>());
|
||||
expect(result2.calendarsToRefresh).toEqual(new Set<string>());
|
||||
expect(result3.calendarsToRefresh).toEqual(new Set<string>());
|
||||
});
|
||||
expect(result1.calendarsToRefresh).toEqual(new Set<string>())
|
||||
expect(result2.calendarsToRefresh).toEqual(new Set<string>())
|
||||
expect(result3.calendarsToRefresh).toEqual(new Set<string>())
|
||||
})
|
||||
|
||||
it("should handle registered event", () => {
|
||||
it('should handle registered event', () => {
|
||||
const message = {
|
||||
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: [
|
||||
"/calendars/cal1/entry1",
|
||||
"/calendars/cal2/entry2",
|
||||
],
|
||||
};
|
||||
'/calendars/cal1/entry1',
|
||||
'/calendars/cal2/entry2'
|
||||
]
|
||||
}
|
||||
|
||||
const result = parseMessage(message);
|
||||
const result = parseMessage(message)
|
||||
|
||||
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
|
||||
expect(result.calendarsToRefresh).toContain("/calendars/cal2/entry2");
|
||||
expect(result.calendarsToRefresh.size).toBe(2);
|
||||
});
|
||||
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
|
||||
expect(result.calendarsToRefresh).toContain('/calendars/cal2/entry2')
|
||||
expect(result.calendarsToRefresh.size).toBe(2)
|
||||
})
|
||||
|
||||
it("should handle unregistered event", () => {
|
||||
it('should handle unregistered event', () => {
|
||||
const message = {
|
||||
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ["/calendars/cal1/entry1"],
|
||||
};
|
||||
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ['/calendars/cal1/entry1']
|
||||
}
|
||||
|
||||
const result = parseMessage(message);
|
||||
const result = parseMessage(message)
|
||||
|
||||
expect(result.calendarsToHide).toContain("/calendars/cal1/entry1");
|
||||
});
|
||||
expect(result.calendarsToHide).toContain('/calendars/cal1/entry1')
|
||||
})
|
||||
|
||||
it("should handle calendar path updates", () => {
|
||||
it('should handle calendar path updates', () => {
|
||||
const message = {
|
||||
"/calendars/cal1/entry1": { updated: true },
|
||||
};
|
||||
'/calendars/cal1/entry1': { updated: true }
|
||||
}
|
||||
|
||||
const result = parseMessage(message);
|
||||
const result = parseMessage(message)
|
||||
|
||||
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
|
||||
expect(result.calendarsToRefresh.size).toBe(1);
|
||||
});
|
||||
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
|
||||
expect(result.calendarsToRefresh.size).toBe(1)
|
||||
})
|
||||
|
||||
it("should parse multiple calendar paths", () => {
|
||||
it('should parse multiple calendar paths', () => {
|
||||
const message = {
|
||||
"/calendars/cal1/entry1": {},
|
||||
"/calendars/cal2/entry2": {},
|
||||
};
|
||||
'/calendars/cal1/entry1': {},
|
||||
'/calendars/cal2/entry2': {}
|
||||
}
|
||||
|
||||
const result = parseMessage(message);
|
||||
const result = parseMessage(message)
|
||||
|
||||
expect(result.calendarsToRefresh.size).toBe(2);
|
||||
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
|
||||
expect(result.calendarsToRefresh).toContain("/calendars/cal2/entry2");
|
||||
});
|
||||
expect(result.calendarsToRefresh.size).toBe(2)
|
||||
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
|
||||
expect(result.calendarsToRefresh).toContain('/calendars/cal2/entry2')
|
||||
})
|
||||
|
||||
it("should handle multiple event types in single message", () => {
|
||||
it('should handle multiple event types in single message', () => {
|
||||
const message = {
|
||||
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: ["/calendars/cal1/entry1"],
|
||||
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ["/calendars/cal2/entry2"],
|
||||
"/calendars/cal1/entry1": {},
|
||||
};
|
||||
[WS_INBOUND_EVENTS.CLIENT_REGISTERED]: ['/calendars/cal1/entry1'],
|
||||
[WS_INBOUND_EVENTS.CLIENT_UNREGISTERED]: ['/calendars/cal2/entry2'],
|
||||
'/calendars/cal1/entry1': {}
|
||||
}
|
||||
|
||||
const result = parseMessage(message);
|
||||
const result = parseMessage(message)
|
||||
|
||||
expect(result.calendarsToRefresh.size).toBe(1);
|
||||
expect(result.calendarsToRefresh).toContain("/calendars/cal1/entry1");
|
||||
});
|
||||
});
|
||||
expect(result.calendarsToRefresh.size).toBe(1)
|
||||
expect(result.calendarsToRefresh).toContain('/calendars/cal1/entry1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
import { registerToCalendars } from "@/websocket/operations/registerToCalendars";
|
||||
import { registerToCalendars } from '@/websocket/operations/registerToCalendars'
|
||||
|
||||
describe("registerToCalendars", () => {
|
||||
let mockSocket: any;
|
||||
describe('registerToCalendars', () => {
|
||||
let mockSocket: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockSocket = {
|
||||
readyState: WebSocket.OPEN,
|
||||
send: jest.fn(),
|
||||
};
|
||||
});
|
||||
send: jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
it("should send registration message with calendar URIs", () => {
|
||||
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
|
||||
it('should send registration message with calendar URIs', () => {
|
||||
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
|
||||
|
||||
registerToCalendars(mockSocket, calendarURIs);
|
||||
registerToCalendars(mockSocket, calendarURIs)
|
||||
|
||||
expect(mockSocket.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
register: calendarURIs,
|
||||
register: calendarURIs
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if socket is not open", () => {
|
||||
mockSocket.readyState = WebSocket.CONNECTING;
|
||||
const calendarURIs = ["/calendars/cal1"];
|
||||
it('should throw error if socket is not open', () => {
|
||||
mockSocket.readyState = WebSocket.CONNECTING
|
||||
const calendarURIs = ['/calendars/cal1']
|
||||
|
||||
expect(() => registerToCalendars(mockSocket, calendarURIs)).toThrow(
|
||||
"Cannot register: WebSocket is not open"
|
||||
);
|
||||
});
|
||||
'Cannot register: WebSocket is not open'
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle empty calendar list", () => {
|
||||
registerToCalendars(mockSocket, []);
|
||||
it('should handle empty calendar list', () => {
|
||||
registerToCalendars(mockSocket, [])
|
||||
|
||||
expect(mockSocket.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
register: [],
|
||||
register: []
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should log registration", () => {
|
||||
const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation();
|
||||
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
|
||||
it('should log registration', () => {
|
||||
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation()
|
||||
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
|
||||
|
||||
registerToCalendars(mockSocket, calendarURIs);
|
||||
registerToCalendars(mockSocket, calendarURIs)
|
||||
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Registered to calendars",
|
||||
'Registered to calendars',
|
||||
calendarURIs
|
||||
);
|
||||
)
|
||||
|
||||
consoleInfoSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
consoleInfoSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import { unregisterToCalendars } from "@/websocket/operations/unregisterToCalendars";
|
||||
import { unregisterToCalendars } from '@/websocket/operations/unregisterToCalendars'
|
||||
|
||||
describe("unregisterToCalendars", () => {
|
||||
let mockSocket: any;
|
||||
describe('unregisterToCalendars', () => {
|
||||
let mockSocket: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockSocket = {
|
||||
readyState: WebSocket.OPEN,
|
||||
send: jest.fn(),
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
send: jest.fn()
|
||||
}
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should send unregistration message with calendar URIs", () => {
|
||||
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
|
||||
it('should send unregistration message with calendar URIs', () => {
|
||||
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
|
||||
|
||||
unregisterToCalendars(mockSocket, calendarURIs);
|
||||
unregisterToCalendars(mockSocket, calendarURIs)
|
||||
|
||||
expect(mockSocket.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
unregister: calendarURIs,
|
||||
unregister: calendarURIs
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if socket is not open", () => {
|
||||
mockSocket.readyState = WebSocket.CONNECTING;
|
||||
const calendarURIs = ["/calendars/cal1"];
|
||||
it('should throw error if socket is not open', () => {
|
||||
mockSocket.readyState = WebSocket.CONNECTING
|
||||
const calendarURIs = ['/calendars/cal1']
|
||||
|
||||
expect(() => unregisterToCalendars(mockSocket, calendarURIs)).toThrow(
|
||||
"Cannot unregister: WebSocket is not open"
|
||||
);
|
||||
});
|
||||
'Cannot unregister: WebSocket is not open'
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle empty calendar list", () => {
|
||||
unregisterToCalendars(mockSocket, []);
|
||||
it('should handle empty calendar list', () => {
|
||||
unregisterToCalendars(mockSocket, [])
|
||||
|
||||
expect(mockSocket.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
unregister: [],
|
||||
unregister: []
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("should log unregistration", () => {
|
||||
const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation();
|
||||
const calendarURIs = ["/calendars/cal1", "/calendars/cal2"];
|
||||
it('should log unregistration', () => {
|
||||
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation()
|
||||
const calendarURIs = ['/calendars/cal1', '/calendars/cal2']
|
||||
|
||||
unregisterToCalendars(mockSocket, calendarURIs);
|
||||
unregisterToCalendars(mockSocket, calendarURIs)
|
||||
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Unregistered to calendars",
|
||||
'Unregistered to calendars',
|
||||
calendarURIs
|
||||
);
|
||||
)
|
||||
|
||||
consoleInfoSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
consoleInfoSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,72 +2,72 @@ import {
|
||||
calendarRangeManager,
|
||||
getDisplayedCalendarRange,
|
||||
getDisplayedDate,
|
||||
setDisplayedDateAndRange,
|
||||
} from "@/utils/CalendarRangeManager";
|
||||
setDisplayedDateAndRange
|
||||
} from '@/utils/CalendarRangeManager'
|
||||
|
||||
describe("CalendarRangeManager", () => {
|
||||
describe('CalendarRangeManager', () => {
|
||||
beforeEach(() => {
|
||||
setDisplayedDateAndRange(new Date());
|
||||
});
|
||||
setDisplayedDateAndRange(new Date())
|
||||
})
|
||||
|
||||
it("should return a singleton instance", () => {
|
||||
const instance1 = calendarRangeManager;
|
||||
const instance2 = calendarRangeManager;
|
||||
it('should return a singleton instance', () => {
|
||||
const instance1 = calendarRangeManager
|
||||
const instance2 = calendarRangeManager
|
||||
|
||||
expect(instance1).toStrictEqual(instance2);
|
||||
});
|
||||
expect(instance1).toStrictEqual(instance2)
|
||||
})
|
||||
|
||||
it("should compute and return calendar range when date is set", () => {
|
||||
const testDate = new Date("2025-06-15T10:00:00Z");
|
||||
setDisplayedDateAndRange(testDate);
|
||||
it('should compute and return calendar range when date is set', () => {
|
||||
const testDate = new Date('2025-06-15T10:00:00Z')
|
||||
setDisplayedDateAndRange(testDate)
|
||||
|
||||
const range = getDisplayedCalendarRange();
|
||||
expect(range.start).toBeInstanceOf(Date);
|
||||
expect(range.end).toBeInstanceOf(Date);
|
||||
expect(range.start.getTime()).toBeLessThanOrEqual(testDate.getTime());
|
||||
expect(range.end.getTime()).toBeGreaterThanOrEqual(testDate.getTime());
|
||||
});
|
||||
const range = getDisplayedCalendarRange()
|
||||
expect(range.start).toBeInstanceOf(Date)
|
||||
expect(range.end).toBeInstanceOf(Date)
|
||||
expect(range.start.getTime()).toBeLessThanOrEqual(testDate.getTime())
|
||||
expect(range.end.getTime()).toBeGreaterThanOrEqual(testDate.getTime())
|
||||
})
|
||||
|
||||
it("should get the default date", () => {
|
||||
const date = getDisplayedDate();
|
||||
expect(date).toBeInstanceOf(Date);
|
||||
});
|
||||
it('should get the default date', () => {
|
||||
const date = getDisplayedDate()
|
||||
expect(date).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
it("should set and get a date", () => {
|
||||
const testDate = new Date("2025-01-15T10:00:00Z");
|
||||
setDisplayedDateAndRange(testDate);
|
||||
it('should set and get a date', () => {
|
||||
const testDate = new Date('2025-01-15T10:00:00Z')
|
||||
setDisplayedDateAndRange(testDate)
|
||||
|
||||
const retrievedDate = getDisplayedDate();
|
||||
expect(retrievedDate).toStrictEqual(testDate);
|
||||
});
|
||||
const retrievedDate = getDisplayedDate()
|
||||
expect(retrievedDate).toStrictEqual(testDate)
|
||||
})
|
||||
|
||||
it("should persist date across multiple calls", () => {
|
||||
const testDate = new Date("2025-06-20T15:30:00Z");
|
||||
setDisplayedDateAndRange(testDate);
|
||||
it('should persist date across multiple calls', () => {
|
||||
const testDate = new Date('2025-06-20T15:30:00Z')
|
||||
setDisplayedDateAndRange(testDate)
|
||||
|
||||
const date1 = getDisplayedDate();
|
||||
const date2 = getDisplayedDate();
|
||||
const date1 = getDisplayedDate()
|
||||
const date2 = getDisplayedDate()
|
||||
|
||||
expect(date1).toStrictEqual(date2);
|
||||
expect(date1).toStrictEqual(testDate);
|
||||
});
|
||||
expect(date1).toStrictEqual(date2)
|
||||
expect(date1).toStrictEqual(testDate)
|
||||
})
|
||||
|
||||
it("should update date when set multiple times", () => {
|
||||
const date1 = new Date("2025-01-01T00:00:00Z");
|
||||
const date2 = new Date("2025-12-31T23:59:59Z");
|
||||
it('should update date when set multiple times', () => {
|
||||
const date1 = new Date('2025-01-01T00:00:00Z')
|
||||
const date2 = new Date('2025-12-31T23:59:59Z')
|
||||
|
||||
setDisplayedDateAndRange(date1);
|
||||
expect(getDisplayedDate()).toStrictEqual(date1);
|
||||
setDisplayedDateAndRange(date1)
|
||||
expect(getDisplayedDate()).toStrictEqual(date1)
|
||||
|
||||
setDisplayedDateAndRange(date2);
|
||||
expect(getDisplayedDate()).toStrictEqual(date2);
|
||||
});
|
||||
setDisplayedDateAndRange(date2)
|
||||
expect(getDisplayedDate()).toStrictEqual(date2)
|
||||
})
|
||||
|
||||
it("should maintain shared state (singleton behavior)", () => {
|
||||
const testDate = new Date("2025-03-01");
|
||||
setDisplayedDateAndRange(testDate);
|
||||
it('should maintain shared state (singleton behavior)', () => {
|
||||
const testDate = new Date('2025-03-01')
|
||||
setDisplayedDateAndRange(testDate)
|
||||
|
||||
// Verify CalendarRangeManager reflects the same state
|
||||
expect(calendarRangeManager.getDate()).toStrictEqual(testDate);
|
||||
});
|
||||
});
|
||||
expect(calendarRangeManager.getDate()).toStrictEqual(testDate)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { AppStore, RootState } from "@/app/store";
|
||||
import { setupStore } from "@/app/store";
|
||||
import { TwakeMuiThemeProvider } from "@linagora/twake-mui";
|
||||
import type { RenderOptions } from "@testing-library/react";
|
||||
import { render } from "@testing-library/react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { I18nContext } from "twake-i18n";
|
||||
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
|
||||
preloadedState?: Partial<RootState>;
|
||||
store?: AppStore;
|
||||
import type { AppStore, RootState } from '@/app/store'
|
||||
import { setupStore } from '@/app/store'
|
||||
import { TwakeMuiThemeProvider } from '@linagora/twake-mui'
|
||||
import type { RenderOptions } from '@testing-library/react'
|
||||
import { render } from '@testing-library/react'
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { I18nContext } from 'twake-i18n'
|
||||
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
|
||||
preloadedState?: Partial<RootState>
|
||||
store?: AppStore
|
||||
}
|
||||
|
||||
export function renderWithProviders(
|
||||
@@ -18,7 +18,7 @@ export function renderWithProviders(
|
||||
extendedRenderOptions: ExtendedRenderOptions = {}
|
||||
) {
|
||||
const { store = setupStore(preloadedState), ...renderOptions } =
|
||||
extendedRenderOptions;
|
||||
extendedRenderOptions
|
||||
|
||||
const Wrapper = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
@@ -26,31 +26,31 @@ export function renderWithProviders(
|
||||
<I18nContext.Provider
|
||||
value={{
|
||||
t: (key: string, vars?: Record<string, string>) => {
|
||||
if (key === "locale") {
|
||||
return "en";
|
||||
if (key === 'locale') {
|
||||
return 'en'
|
||||
}
|
||||
if (vars) {
|
||||
const params = Object.entries(vars)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(",");
|
||||
return `${key}(${params})`;
|
||||
.join(',')
|
||||
return `${key}(${params})`
|
||||
}
|
||||
return key;
|
||||
return key
|
||||
},
|
||||
f: (date: Date, formatStr: string) => date.toString(),
|
||||
lang: "en",
|
||||
lang: 'en'
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={["/"]}>
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</I18nContext.Provider>
|
||||
</TwakeMuiThemeProvider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
...render(ui, { wrapper: Wrapper, ...renderOptions }),
|
||||
};
|
||||
...render(ui, { wrapper: Wrapper, ...renderOptions })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
import { getInitials, stringToGradient } from "@/utils/avatarUtils";
|
||||
import { getInitials, stringToGradient } from '@/utils/avatarUtils'
|
||||
|
||||
jest.mock("@linagora/twake-mui", () => ({
|
||||
jest.mock('@linagora/twake-mui', () => ({
|
||||
nameToColor: jest.fn((name: string) => {
|
||||
if (!name) return undefined;
|
||||
if (!name) return undefined
|
||||
const colors = [
|
||||
"sunrise",
|
||||
"downy",
|
||||
"sugarCoral",
|
||||
"pinkBonnet",
|
||||
"blueMana",
|
||||
"nightBlue",
|
||||
"snowPea",
|
||||
"pluviophile",
|
||||
"cornflower",
|
||||
"paleGreen",
|
||||
"moonBlue",
|
||||
];
|
||||
'sunrise',
|
||||
'downy',
|
||||
'sugarCoral',
|
||||
'pinkBonnet',
|
||||
'blueMana',
|
||||
'nightBlue',
|
||||
'snowPea',
|
||||
'pluviophile',
|
||||
'cornflower',
|
||||
'paleGreen',
|
||||
'moonBlue'
|
||||
]
|
||||
const hash = Array.from(name.toUpperCase())
|
||||
.map((letter) => letter.charCodeAt(0))
|
||||
.reduce((sum, number) => sum + number, 0);
|
||||
return colors[hash % colors.length];
|
||||
}),
|
||||
}));
|
||||
.map(letter => letter.charCodeAt(0))
|
||||
.reduce((sum, number) => sum + number, 0)
|
||||
return colors[hash % colors.length]
|
||||
})
|
||||
}))
|
||||
|
||||
describe("avatarUtils", () => {
|
||||
describe("getInitials", () => {
|
||||
it("returns 2-letter initials for full name", () => {
|
||||
expect(getInitials("John Doe")).toBe("JD");
|
||||
expect(getInitials("Alice Smith")).toBe("AS");
|
||||
expect(getInitials("Nguyễn Văn")).toBe("NV");
|
||||
});
|
||||
describe('avatarUtils', () => {
|
||||
describe('getInitials', () => {
|
||||
it('returns 2-letter initials for full name', () => {
|
||||
expect(getInitials('John Doe')).toBe('JD')
|
||||
expect(getInitials('Alice Smith')).toBe('AS')
|
||||
expect(getInitials('Nguyễn Văn')).toBe('NV')
|
||||
})
|
||||
|
||||
it("returns single letter for single word", () => {
|
||||
expect(getInitials("Alice")).toBe("A");
|
||||
expect(getInitials("John")).toBe("J");
|
||||
});
|
||||
it('returns single letter for single word', () => {
|
||||
expect(getInitials('Alice')).toBe('A')
|
||||
expect(getInitials('John')).toBe('J')
|
||||
})
|
||||
|
||||
it("returns uppercase initials", () => {
|
||||
expect(getInitials("john doe")).toBe("JD");
|
||||
expect(getInitials("ALICE SMITH")).toBe("AS");
|
||||
expect(getInitials("a")).toBe("A");
|
||||
});
|
||||
it('returns uppercase initials', () => {
|
||||
expect(getInitials('john doe')).toBe('JD')
|
||||
expect(getInitials('ALICE SMITH')).toBe('AS')
|
||||
expect(getInitials('a')).toBe('A')
|
||||
})
|
||||
|
||||
it("handles multiple spaces between words", () => {
|
||||
expect(getInitials("John Doe")).toBe("JD");
|
||||
expect(getInitials("Alice Smith Brown")).toBe("AS");
|
||||
});
|
||||
it('handles multiple spaces between words', () => {
|
||||
expect(getInitials('John Doe')).toBe('JD')
|
||||
expect(getInitials('Alice Smith Brown')).toBe('AS')
|
||||
})
|
||||
|
||||
it("handles email addresses", () => {
|
||||
expect(getInitials("john.doe@email.com")).toBe("J");
|
||||
expect(getInitials("test@example.com")).toBe("T");
|
||||
});
|
||||
it('handles email addresses', () => {
|
||||
expect(getInitials('john.doe@email.com')).toBe('J')
|
||||
expect(getInitials('test@example.com')).toBe('T')
|
||||
})
|
||||
|
||||
it("handles empty string", () => {
|
||||
expect(getInitials("")).toBe("");
|
||||
});
|
||||
it('handles empty string', () => {
|
||||
expect(getInitials('')).toBe('')
|
||||
})
|
||||
|
||||
it("handles whitespace-only string", () => {
|
||||
expect(getInitials(" ")).toBe("");
|
||||
});
|
||||
it('handles whitespace-only string', () => {
|
||||
expect(getInitials(' ')).toBe('')
|
||||
})
|
||||
|
||||
it("handles single character", () => {
|
||||
expect(getInitials("A")).toBe("A");
|
||||
expect(getInitials("a")).toBe("A");
|
||||
});
|
||||
it('handles single character', () => {
|
||||
expect(getInitials('A')).toBe('A')
|
||||
expect(getInitials('a')).toBe('A')
|
||||
})
|
||||
|
||||
it("handles names with special characters", () => {
|
||||
expect(getInitials("Jean-Pierre")).toBe("J");
|
||||
expect(getInitials("Mary Jane Watson")).toBe("MJ");
|
||||
});
|
||||
it('handles names with special characters', () => {
|
||||
expect(getInitials('Jean-Pierre')).toBe('J')
|
||||
expect(getInitials('Mary Jane Watson')).toBe('MJ')
|
||||
})
|
||||
|
||||
it("handles unicode characters", () => {
|
||||
expect(getInitials("Nguyễn Văn")).toBe("NV");
|
||||
expect(getInitials("José María")).toBe("JM");
|
||||
});
|
||||
it('handles unicode characters', () => {
|
||||
expect(getInitials('Nguyễn Văn')).toBe('NV')
|
||||
expect(getInitials('José María')).toBe('JM')
|
||||
})
|
||||
|
||||
it("takes first two words for names with more than two words", () => {
|
||||
expect(getInitials("John Michael Smith")).toBe("JM");
|
||||
expect(getInitials("Alice Bob Charlie")).toBe("AB");
|
||||
});
|
||||
});
|
||||
it('takes first two words for names with more than two words', () => {
|
||||
expect(getInitials('John Michael Smith')).toBe('JM')
|
||||
expect(getInitials('Alice Bob Charlie')).toBe('AB')
|
||||
})
|
||||
})
|
||||
|
||||
describe("stringToGradient", () => {
|
||||
it("returns color for valid string", () => {
|
||||
const result = stringToGradient("John Doe");
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
});
|
||||
describe('stringToGradient', () => {
|
||||
it('returns color for valid string', () => {
|
||||
const result = stringToGradient('John Doe')
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result).toBe('string')
|
||||
})
|
||||
|
||||
it("returns same color for same input", () => {
|
||||
const result1 = stringToGradient("John Doe");
|
||||
const result2 = stringToGradient("John Doe");
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
it('returns same color for same input', () => {
|
||||
const result1 = stringToGradient('John Doe')
|
||||
const result2 = stringToGradient('John Doe')
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
it("returns undefined for empty string", () => {
|
||||
expect(stringToGradient("")).toBeUndefined();
|
||||
});
|
||||
it('returns undefined for empty string', () => {
|
||||
expect(stringToGradient('')).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns color for single character", () => {
|
||||
const result = stringToGradient("A");
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
});
|
||||
it('returns color for single character', () => {
|
||||
const result = stringToGradient('A')
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result).toBe('string')
|
||||
})
|
||||
|
||||
it("returns color for email address", () => {
|
||||
const result = stringToGradient("test@example.com");
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
});
|
||||
});
|
||||
});
|
||||
it('returns color for email address', () => {
|
||||
const result = stringToGradient('test@example.com')
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result).toBe('string')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { buildDelegatedEventURL } from "@/features/Events/utils";
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { buildDelegatedEventURL } from '@/features/Events/utils'
|
||||
|
||||
const makeCalendar = (link: string): Calendar =>
|
||||
({
|
||||
id: "user2/cal1",
|
||||
id: 'user2/cal1',
|
||||
delegated: true,
|
||||
link,
|
||||
owner: { emails: ["owner@example.com"] },
|
||||
}) as Calendar;
|
||||
owner: { emails: ['owner@example.com'] }
|
||||
}) as Calendar
|
||||
|
||||
describe("buildDelegatedEventURL", () => {
|
||||
it("rebases event filename onto calendar link base path", () => {
|
||||
const calendar = makeCalendar("/calendars/user2/cal1.json");
|
||||
const eventURL = "/calendars/someother/path/event-abc.ics";
|
||||
describe('buildDelegatedEventURL', () => {
|
||||
it('rebases event filename onto calendar link base path', () => {
|
||||
const calendar = makeCalendar('/calendars/user2/cal1.json')
|
||||
const eventURL = '/calendars/someother/path/event-abc.ics'
|
||||
expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
|
||||
"/calendars/user2/cal1/event-abc.ics"
|
||||
);
|
||||
});
|
||||
'/calendars/user2/cal1/event-abc.ics'
|
||||
)
|
||||
})
|
||||
|
||||
it("strips .json suffix from calendar link", () => {
|
||||
const calendar = makeCalendar("/calendars/user2/cal1.json");
|
||||
const eventURL = "/calendars/user2/cal1/event-abc.ics";
|
||||
const result = buildDelegatedEventURL(calendar, eventURL);
|
||||
expect(result).not.toContain(".json");
|
||||
});
|
||||
it('strips .json suffix from calendar link', () => {
|
||||
const calendar = makeCalendar('/calendars/user2/cal1.json')
|
||||
const eventURL = '/calendars/user2/cal1/event-abc.ics'
|
||||
const result = buildDelegatedEventURL(calendar, eventURL)
|
||||
expect(result).not.toContain('.json')
|
||||
})
|
||||
|
||||
it("preserves the exact filename from the event URL", () => {
|
||||
const calendar = makeCalendar("/calendars/user2/cal1.json");
|
||||
const eventURL = "/calendars/user2/cal1/some-uid-with-dashes.ics";
|
||||
it('preserves the exact filename from the event URL', () => {
|
||||
const calendar = makeCalendar('/calendars/user2/cal1.json')
|
||||
const eventURL = '/calendars/user2/cal1/some-uid-with-dashes.ics'
|
||||
expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
|
||||
"/calendars/user2/cal1/some-uid-with-dashes.ics"
|
||||
);
|
||||
});
|
||||
'/calendars/user2/cal1/some-uid-with-dashes.ics'
|
||||
)
|
||||
})
|
||||
|
||||
it("throws when event URL has no filename", () => {
|
||||
const calendar = makeCalendar("/calendars/user2/cal1.json");
|
||||
const eventURL = "";
|
||||
it('throws when event URL has no filename', () => {
|
||||
const calendar = makeCalendar('/calendars/user2/cal1.json')
|
||||
const eventURL = ''
|
||||
expect(() => buildDelegatedEventURL(calendar, eventURL)).toThrow(
|
||||
/Cannot extract filename from event URL/
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
it("works with nested calendar link paths", () => {
|
||||
const calendar = makeCalendar("/dav/calendars/users/user2/cal1.json");
|
||||
const eventURL = "/other/path/event-xyz.ics";
|
||||
it('works with nested calendar link paths', () => {
|
||||
const calendar = makeCalendar('/dav/calendars/users/user2/cal1.json')
|
||||
const eventURL = '/other/path/event-xyz.ics'
|
||||
expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
|
||||
"/dav/calendars/users/user2/cal1/event-xyz.ics"
|
||||
);
|
||||
});
|
||||
});
|
||||
'/dav/calendars/users/user2/cal1/event-xyz.ics'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,231 +1,231 @@
|
||||
import {
|
||||
eventToFullCalendarFormat,
|
||||
getCalendarVisibility,
|
||||
} from "@/components/Calendar/utils/calendarUtils";
|
||||
import { Calendar, DelegationAccess } from "@/features/Calendars/CalendarTypes";
|
||||
import { AclEntry } from "@/features/Calendars/types/CalendarData";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { userOrganiser } from "@/features/User/userDataTypes";
|
||||
getCalendarVisibility
|
||||
} from '@/components/Calendar/utils/calendarUtils'
|
||||
import { Calendar, DelegationAccess } from '@/features/Calendars/CalendarTypes'
|
||||
import { AclEntry } from '@/features/Calendars/types/CalendarData'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import { userOrganiser } from '@/features/User/userDataTypes'
|
||||
|
||||
describe("getCalendarVisibility", () => {
|
||||
describe('getCalendarVisibility', () => {
|
||||
it("returns 'public' when {DAV:}authenticated has {DAV:}read", () => {
|
||||
const acl: AclEntry[] = [
|
||||
{
|
||||
privilege: "{DAV:}read",
|
||||
principal: "{DAV:}authenticated",
|
||||
protected: true,
|
||||
},
|
||||
];
|
||||
expect(getCalendarVisibility(acl)).toBe("public");
|
||||
});
|
||||
privilege: '{DAV:}read',
|
||||
principal: '{DAV:}authenticated',
|
||||
protected: true
|
||||
}
|
||||
]
|
||||
expect(getCalendarVisibility(acl)).toBe('public')
|
||||
})
|
||||
|
||||
it("returns 'private' when {DAV:}authenticated only has read-free-busy", () => {
|
||||
const acl: AclEntry[] = [
|
||||
{
|
||||
privilege: "{urn:ietf:params:xml:ns:caldav}read-free-busy",
|
||||
principal: "{DAV:}authenticated",
|
||||
protected: true,
|
||||
},
|
||||
];
|
||||
expect(getCalendarVisibility(acl)).toBe("private");
|
||||
});
|
||||
privilege: '{urn:ietf:params:xml:ns:caldav}read-free-busy',
|
||||
principal: '{DAV:}authenticated',
|
||||
protected: true
|
||||
}
|
||||
]
|
||||
expect(getCalendarVisibility(acl)).toBe('private')
|
||||
})
|
||||
|
||||
it("returns 'private' when {DAV:}authenticated is not present", () => {
|
||||
const acl: AclEntry[] = [
|
||||
{
|
||||
privilege: "{DAV:}read",
|
||||
principal: "principals/users/123",
|
||||
protected: true,
|
||||
},
|
||||
];
|
||||
expect(getCalendarVisibility(acl)).toBe("private");
|
||||
});
|
||||
privilege: '{DAV:}read',
|
||||
principal: 'principals/users/123',
|
||||
protected: true
|
||||
}
|
||||
]
|
||||
expect(getCalendarVisibility(acl)).toBe('private')
|
||||
})
|
||||
|
||||
it("ignores non-authenticated principals and still returns 'private'", () => {
|
||||
const acl: AclEntry[] = [
|
||||
{
|
||||
privilege: "{DAV:}read",
|
||||
principal: "principals/users/other",
|
||||
protected: true,
|
||||
privilege: '{DAV:}read',
|
||||
principal: 'principals/users/other',
|
||||
protected: true
|
||||
},
|
||||
{
|
||||
privilege: "{DAV:}write",
|
||||
principal: "principals/users/other",
|
||||
protected: true,
|
||||
},
|
||||
];
|
||||
expect(getCalendarVisibility(acl)).toBe("private");
|
||||
});
|
||||
privilege: '{DAV:}write',
|
||||
principal: 'principals/users/other',
|
||||
protected: true
|
||||
}
|
||||
]
|
||||
expect(getCalendarVisibility(acl)).toBe('private')
|
||||
})
|
||||
|
||||
it("stops early when it finds a {DAV:}read for {DAV:}authenticated", () => {
|
||||
it('stops early when it finds a {DAV:}read for {DAV:}authenticated', () => {
|
||||
const acl: AclEntry[] = [
|
||||
{
|
||||
privilege: "{DAV:}read",
|
||||
principal: "{DAV:}authenticated",
|
||||
protected: true,
|
||||
privilege: '{DAV:}read',
|
||||
principal: '{DAV:}authenticated',
|
||||
protected: true
|
||||
},
|
||||
{
|
||||
privilege: "{DAV:}write",
|
||||
principal: "{DAV:}authenticated",
|
||||
protected: true,
|
||||
},
|
||||
];
|
||||
expect(getCalendarVisibility(acl)).toBe("public");
|
||||
});
|
||||
});
|
||||
privilege: '{DAV:}write',
|
||||
principal: '{DAV:}authenticated',
|
||||
protected: true
|
||||
}
|
||||
]
|
||||
expect(getCalendarVisibility(acl)).toBe('public')
|
||||
})
|
||||
})
|
||||
|
||||
jest.mock("twake-i18n", () => ({
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
jest.mock('twake-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key })
|
||||
}))
|
||||
|
||||
const baseEvent: CalendarEvent = {
|
||||
uid: "event-1",
|
||||
calId: "user1/cal1",
|
||||
title: "Test Event",
|
||||
start: "2024-01-15T10:00:00",
|
||||
end: "2024-01-15T11:00:00",
|
||||
uid: 'event-1',
|
||||
calId: 'user1/cal1',
|
||||
title: 'Test Event',
|
||||
start: '2024-01-15T10:00:00',
|
||||
end: '2024-01-15T11:00:00',
|
||||
allday: false,
|
||||
organizer: { cal_address: "owner@example.com" } as userOrganiser,
|
||||
color: { light: "#FF0000" },
|
||||
class: "PUBLIC",
|
||||
} as unknown as CalendarEvent;
|
||||
organizer: { cal_address: 'owner@example.com' } as userOrganiser,
|
||||
color: { light: '#FF0000' },
|
||||
class: 'PUBLIC'
|
||||
} as unknown as CalendarEvent
|
||||
|
||||
const makeCalendar = (overrides: Partial<Calendar> = {}): Calendar =>
|
||||
({
|
||||
id: "user1/cal1",
|
||||
name: "Test Calendar",
|
||||
id: 'user1/cal1',
|
||||
name: 'Test Calendar',
|
||||
delegated: false,
|
||||
access: {} as DelegationAccess,
|
||||
owner: { emails: ["alice@example.com"], firstname: "Alice" },
|
||||
color: { light: "#FF0000" },
|
||||
owner: { emails: ['alice@example.com'], firstname: 'Alice' },
|
||||
color: { light: '#FF0000' },
|
||||
events: {},
|
||||
...overrides,
|
||||
}) as Calendar;
|
||||
...overrides
|
||||
}) as Calendar
|
||||
|
||||
const callFormat = (
|
||||
event: CalendarEvent,
|
||||
calendars: Record<string, Calendar>,
|
||||
userAddress = "alice@example.com",
|
||||
userId = "user1"
|
||||
userAddress = 'alice@example.com',
|
||||
userId = 'user1'
|
||||
) =>
|
||||
eventToFullCalendarFormat([event], [], userId, userAddress, false, calendars);
|
||||
eventToFullCalendarFormat([event], [], userId, userAddress, false, calendars)
|
||||
|
||||
describe("eventToFullCalendarFormat - editable flag", () => {
|
||||
describe("personal calendar (non-delegated)", () => {
|
||||
const calendar = makeCalendar({ delegated: false });
|
||||
describe('eventToFullCalendarFormat - editable flag', () => {
|
||||
describe('personal calendar (non-delegated)', () => {
|
||||
const calendar = makeCalendar({ delegated: false })
|
||||
|
||||
it("is editable when user is organizer", () => {
|
||||
it('is editable when user is organizer', () => {
|
||||
const event = {
|
||||
...baseEvent,
|
||||
organizer: { cal_address: "alice@example.com" },
|
||||
} as CalendarEvent;
|
||||
const [result] = callFormat(event, { "user1/cal1": calendar });
|
||||
expect(result.editable).toBe(true);
|
||||
});
|
||||
organizer: { cal_address: 'alice@example.com' }
|
||||
} as CalendarEvent
|
||||
const [result] = callFormat(event, { 'user1/cal1': calendar })
|
||||
expect(result.editable).toBe(true)
|
||||
})
|
||||
|
||||
it("is not editable when user is not organizer", () => {
|
||||
it('is not editable when user is not organizer', () => {
|
||||
const event = {
|
||||
...baseEvent,
|
||||
organizer: { cal_address: "other@example.com" },
|
||||
} as CalendarEvent;
|
||||
const [result] = callFormat(event, { "user1/cal1": calendar });
|
||||
expect(result.editable).toBe(false);
|
||||
});
|
||||
organizer: { cal_address: 'other@example.com' }
|
||||
} as CalendarEvent
|
||||
const [result] = callFormat(event, { 'user1/cal1': calendar })
|
||||
expect(result.editable).toBe(false)
|
||||
})
|
||||
|
||||
it("is not editable when pending", () => {
|
||||
it('is not editable when pending', () => {
|
||||
const event = {
|
||||
...baseEvent,
|
||||
organizer: { cal_address: "alice@example.com" },
|
||||
} as CalendarEvent;
|
||||
organizer: { cal_address: 'alice@example.com' }
|
||||
} as CalendarEvent
|
||||
const [result] = eventToFullCalendarFormat(
|
||||
[event],
|
||||
[],
|
||||
"user1",
|
||||
"alice@example.com",
|
||||
'user1',
|
||||
'alice@example.com',
|
||||
true,
|
||||
{ "user1/cal1": calendar }
|
||||
);
|
||||
expect(result.editable).toBe(false);
|
||||
});
|
||||
});
|
||||
{ 'user1/cal1': calendar }
|
||||
)
|
||||
expect(result.editable).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delegated calendar", () => {
|
||||
describe('delegated calendar', () => {
|
||||
const writeDelegatedCalendar = makeCalendar({
|
||||
id: "user2/cal1",
|
||||
id: 'user2/cal1',
|
||||
delegated: true,
|
||||
access: { write: true } as DelegationAccess,
|
||||
owner: { emails: ["owner@example.com"], firstname: "Owner" },
|
||||
});
|
||||
owner: { emails: ['owner@example.com'], firstname: 'Owner' }
|
||||
})
|
||||
const readDelegatedCalendar = makeCalendar({
|
||||
id: "user2/cal1",
|
||||
id: 'user2/cal1',
|
||||
delegated: true,
|
||||
access: { read: true } as DelegationAccess,
|
||||
owner: { emails: ["owner@example.com"], firstname: "Owner" },
|
||||
});
|
||||
const event = { ...baseEvent, calId: "user2/cal1" };
|
||||
owner: { emails: ['owner@example.com'], firstname: 'Owner' }
|
||||
})
|
||||
const event = { ...baseEvent, calId: 'user2/cal1' }
|
||||
|
||||
it("is editable when owner is organizer and delegated user has write rights", () => {
|
||||
it('is editable when owner is organizer and delegated user has write rights', () => {
|
||||
const [result] = callFormat(
|
||||
{
|
||||
...event,
|
||||
organizer: { cal_address: "owner@example.com" },
|
||||
organizer: { cal_address: 'owner@example.com' }
|
||||
} as CalendarEvent,
|
||||
{ "user2/cal1": writeDelegatedCalendar }
|
||||
);
|
||||
expect(result.editable).toBe(true);
|
||||
});
|
||||
{ 'user2/cal1': writeDelegatedCalendar }
|
||||
)
|
||||
expect(result.editable).toBe(true)
|
||||
})
|
||||
|
||||
it("is not editable when owner is organizer but delegated user only has read access", () => {
|
||||
it('is not editable when owner is organizer but delegated user only has read access', () => {
|
||||
const [result] = callFormat(
|
||||
{
|
||||
...event,
|
||||
organizer: { cal_address: "owner@example.com" },
|
||||
organizer: { cal_address: 'owner@example.com' }
|
||||
} as CalendarEvent,
|
||||
{ "user2/cal1": readDelegatedCalendar }
|
||||
);
|
||||
expect(result.editable).toBe(false);
|
||||
});
|
||||
{ 'user2/cal1': readDelegatedCalendar }
|
||||
)
|
||||
expect(result.editable).toBe(false)
|
||||
})
|
||||
|
||||
it("is not editable when owner is not organizer", () => {
|
||||
it('is not editable when owner is not organizer', () => {
|
||||
const [result] = callFormat(
|
||||
{
|
||||
...event,
|
||||
organizer: { cal_address: "someone-else@example.com" },
|
||||
organizer: { cal_address: 'someone-else@example.com' }
|
||||
} as CalendarEvent,
|
||||
{ "user2/cal1": writeDelegatedCalendar }
|
||||
);
|
||||
expect(result.editable).toBe(false);
|
||||
});
|
||||
{ 'user2/cal1': writeDelegatedCalendar }
|
||||
)
|
||||
expect(result.editable).toBe(false)
|
||||
})
|
||||
|
||||
it("is not editable when pending even if owner is organizer", () => {
|
||||
it('is not editable when pending even if owner is organizer', () => {
|
||||
const [result] = eventToFullCalendarFormat(
|
||||
[
|
||||
{
|
||||
...event,
|
||||
organizer: { cal_address: "owner@example.com" },
|
||||
} as CalendarEvent,
|
||||
organizer: { cal_address: 'owner@example.com' }
|
||||
} as CalendarEvent
|
||||
],
|
||||
[],
|
||||
"user1",
|
||||
"alice@example.com",
|
||||
'user1',
|
||||
'alice@example.com',
|
||||
true,
|
||||
{ "user2/cal1": writeDelegatedCalendar }
|
||||
);
|
||||
expect(result.editable).toBe(false);
|
||||
});
|
||||
{ 'user2/cal1': writeDelegatedCalendar }
|
||||
)
|
||||
expect(result.editable).toBe(false)
|
||||
})
|
||||
|
||||
it("uses owner email not logged-in user email for organizer check", () => {
|
||||
it('uses owner email not logged-in user email for organizer check', () => {
|
||||
// logged-in user is alice, owner is owner@example.com
|
||||
// organizer matches owner → editable
|
||||
const [result] = callFormat(
|
||||
{
|
||||
...event,
|
||||
organizer: { cal_address: "owner@example.com" },
|
||||
organizer: { cal_address: 'owner@example.com' }
|
||||
} as CalendarEvent,
|
||||
{ "user2/cal1": writeDelegatedCalendar },
|
||||
"alice@example.com" // logged-in user, should NOT be used for delegated
|
||||
);
|
||||
expect(result.editable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
{ 'user2/cal1': writeDelegatedCalendar },
|
||||
'alice@example.com' // logged-in user, should NOT be used for delegated
|
||||
)
|
||||
expect(result.editable).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,120 +1,120 @@
|
||||
import { createEventContext } from "@/features/Events/createEventContext";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { userData } from "@/features/User/userDataTypes";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { createEventContext } from '@/features/Events/createEventContext'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import { userData } from '@/features/User/userDataTypes'
|
||||
import { userAttendee } from '@/features/User/models/attendee'
|
||||
|
||||
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 makeEvent = (overrides: Partial<CalendarEvent> = {}): CalendarEvent =>
|
||||
({
|
||||
uid: "event-1",
|
||||
calId: "user1/cal1",
|
||||
title: "Test Event",
|
||||
start: "2024-01-15T10:00:00",
|
||||
end: "2024-01-15T11:00:00",
|
||||
organizer: { cal_address: "alice@example.com" },
|
||||
uid: 'event-1',
|
||||
calId: 'user1/cal1',
|
||||
title: 'Test Event',
|
||||
start: '2024-01-15T10:00:00',
|
||||
end: '2024-01-15T11:00:00',
|
||||
organizer: { cal_address: 'alice@example.com' },
|
||||
attendee: [
|
||||
{ cal_address: "alice@example.com", partstat: "ACCEPTED" },
|
||||
{ cal_address: "owner@example.com", partstat: "ACCEPTED" },
|
||||
{ cal_address: "other@example.com", partstat: "NEEDS-ACTION" },
|
||||
{ cal_address: 'alice@example.com', partstat: 'ACCEPTED' },
|
||||
{ cal_address: 'owner@example.com', partstat: 'ACCEPTED' },
|
||||
{ cal_address: 'other@example.com', partstat: 'NEEDS-ACTION' }
|
||||
],
|
||||
...overrides,
|
||||
}) as CalendarEvent;
|
||||
...overrides
|
||||
}) as CalendarEvent
|
||||
|
||||
const makeCalendar = (overrides: Partial<Calendar> = {}): Calendar =>
|
||||
({
|
||||
id: "user1/cal1",
|
||||
name: "Test Calendar",
|
||||
id: 'user1/cal1',
|
||||
name: 'Test Calendar',
|
||||
delegated: false,
|
||||
owner: { emails: ["alice@example.com"], firstname: "Alice" },
|
||||
owner: { emails: ['alice@example.com'], firstname: 'Alice' },
|
||||
events: {},
|
||||
...overrides,
|
||||
}) as Calendar;
|
||||
...overrides
|
||||
}) as Calendar
|
||||
|
||||
describe("createEventContext", () => {
|
||||
describe("non-delegated calendar", () => {
|
||||
const calendar = makeCalendar({ delegated: false });
|
||||
const user = makeUser("alice@example.com");
|
||||
describe('createEventContext', () => {
|
||||
describe('non-delegated calendar', () => {
|
||||
const calendar = makeCalendar({ delegated: false })
|
||||
const user = makeUser('alice@example.com')
|
||||
|
||||
it("finds currentUserAttendee by logged-in user email", () => {
|
||||
const event = makeEvent();
|
||||
const ctx = createEventContext(event, calendar, user);
|
||||
expect(ctx.currentUserAttendee?.cal_address).toBe("alice@example.com");
|
||||
});
|
||||
it('finds currentUserAttendee by logged-in user email', () => {
|
||||
const event = makeEvent()
|
||||
const ctx = createEventContext(event, calendar, user)
|
||||
expect(ctx.currentUserAttendee?.cal_address).toBe('alice@example.com')
|
||||
})
|
||||
|
||||
it("returns null currentUserAttendee when user is not an attendee", () => {
|
||||
const event = makeEvent({ attendee: [] });
|
||||
const ctx = createEventContext(event, calendar, user);
|
||||
expect(ctx.currentUserAttendee).toBeUndefined();
|
||||
});
|
||||
it('returns null currentUserAttendee when user is not an attendee', () => {
|
||||
const event = makeEvent({ attendee: [] })
|
||||
const ctx = createEventContext(event, calendar, user)
|
||||
expect(ctx.currentUserAttendee).toBeUndefined()
|
||||
})
|
||||
|
||||
it("isOwn is true when user email is in owner emails", () => {
|
||||
const ctx = createEventContext(makeEvent(), calendar, user);
|
||||
expect(ctx.isOwn).toBe(true);
|
||||
});
|
||||
it('isOwn is true when user email is in owner emails', () => {
|
||||
const ctx = createEventContext(makeEvent(), calendar, user)
|
||||
expect(ctx.isOwn).toBe(true)
|
||||
})
|
||||
|
||||
it("isOwn is false when user email is not in owner emails", () => {
|
||||
const other = makeUser("other@example.com");
|
||||
const ctx = createEventContext(makeEvent(), calendar, other);
|
||||
expect(ctx.isOwn).toBe(false);
|
||||
});
|
||||
});
|
||||
it('isOwn is false when user email is not in owner emails', () => {
|
||||
const other = makeUser('other@example.com')
|
||||
const ctx = createEventContext(makeEvent(), calendar, other)
|
||||
expect(ctx.isOwn).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delegated calendar", () => {
|
||||
describe('delegated calendar', () => {
|
||||
const calendar = makeCalendar({
|
||||
id: "user2/cal1",
|
||||
id: 'user2/cal1',
|
||||
delegated: true,
|
||||
owner: { emails: ["owner@example.com"], firstname: "Owner" },
|
||||
});
|
||||
const user = makeUser("alice@example.com"); // logged-in user, not the owner
|
||||
owner: { emails: ['owner@example.com'], firstname: 'Owner' }
|
||||
})
|
||||
const user = makeUser('alice@example.com') // logged-in user, not the owner
|
||||
|
||||
it("finds currentUserAttendee by owner email, not logged-in user email", () => {
|
||||
const event = makeEvent({ calId: "user2/cal1" });
|
||||
const ctx = createEventContext(event, calendar, user);
|
||||
expect(ctx.currentUserAttendee?.cal_address).toBe("owner@example.com");
|
||||
});
|
||||
it('finds currentUserAttendee by owner email, not logged-in user email', () => {
|
||||
const event = makeEvent({ calId: 'user2/cal1' })
|
||||
const ctx = createEventContext(event, calendar, user)
|
||||
expect(ctx.currentUserAttendee?.cal_address).toBe('owner@example.com')
|
||||
})
|
||||
|
||||
it("does not find logged-in user as attendee for delegated calendar", () => {
|
||||
it('does not find logged-in user as attendee for delegated calendar', () => {
|
||||
const event = makeEvent({
|
||||
calId: "user2/cal1",
|
||||
calId: 'user2/cal1',
|
||||
attendee: [
|
||||
{
|
||||
cal_address: "owner@example.com",
|
||||
partstat: "ACCEPTED",
|
||||
} as userAttendee,
|
||||
],
|
||||
});
|
||||
const ctx = createEventContext(event, calendar, user);
|
||||
cal_address: 'owner@example.com',
|
||||
partstat: 'ACCEPTED'
|
||||
} as userAttendee
|
||||
]
|
||||
})
|
||||
const ctx = createEventContext(event, calendar, user)
|
||||
// currentUserAttendee should be owner's, not alice's
|
||||
expect(ctx.currentUserAttendee?.cal_address).toBe("owner@example.com");
|
||||
});
|
||||
expect(ctx.currentUserAttendee?.cal_address).toBe('owner@example.com')
|
||||
})
|
||||
|
||||
it("returns undefined currentUserAttendee when owner is not an attendee", () => {
|
||||
it('returns undefined currentUserAttendee when owner is not an attendee', () => {
|
||||
const event = makeEvent({
|
||||
calId: "user2/cal1",
|
||||
calId: 'user2/cal1',
|
||||
attendee: [
|
||||
{
|
||||
cal_address: "alice@example.com",
|
||||
partstat: "ACCEPTED",
|
||||
} as userAttendee,
|
||||
],
|
||||
});
|
||||
const ctx = createEventContext(event, calendar, user);
|
||||
expect(ctx.currentUserAttendee).toBeUndefined();
|
||||
});
|
||||
cal_address: 'alice@example.com',
|
||||
partstat: 'ACCEPTED'
|
||||
} as userAttendee
|
||||
]
|
||||
})
|
||||
const ctx = createEventContext(event, calendar, user)
|
||||
expect(ctx.currentUserAttendee).toBeUndefined()
|
||||
})
|
||||
|
||||
it("passes event through unchanged", () => {
|
||||
const event = makeEvent({ calId: "user2/cal1" });
|
||||
const ctx = createEventContext(event, calendar, user);
|
||||
expect(ctx.event).toBe(event);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('passes event through unchanged', () => {
|
||||
const event = makeEvent({ calId: 'user2/cal1' })
|
||||
const ctx = createEventContext(event, calendar, user)
|
||||
expect(ctx.event).toBe(event)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { getCalendarRange } from "@/utils/dateUtils";
|
||||
import { getCalendarRange } from '@/utils/dateUtils'
|
||||
|
||||
describe("getCalendarRange", () => {
|
||||
it("Nov 2025 (5 weeks): 2025-10-27 to 2025-11-30", () => {
|
||||
describe('getCalendarRange', () => {
|
||||
it('Nov 2025 (5 weeks): 2025-10-27 to 2025-11-30', () => {
|
||||
const { start, end } = getCalendarRange(
|
||||
new Date("2025-11-01T00:00:00.000Z")
|
||||
);
|
||||
expect(start.toISOString().slice(0, 10)).toBe("2025-10-27");
|
||||
expect(end.toISOString().slice(0, 10)).toBe("2025-11-30");
|
||||
});
|
||||
new Date('2025-11-01T00:00:00.000Z')
|
||||
)
|
||||
expect(start.toISOString().slice(0, 10)).toBe('2025-10-27')
|
||||
expect(end.toISOString().slice(0, 10)).toBe('2025-11-30')
|
||||
})
|
||||
|
||||
it("Dec 2025 (6 weeks): end Sunday 2026-01-04", () => {
|
||||
it('Dec 2025 (6 weeks): end Sunday 2026-01-04', () => {
|
||||
const { start, end } = getCalendarRange(
|
||||
new Date("2025-12-01T00:00:00.000Z")
|
||||
);
|
||||
expect(start.toISOString().slice(0, 10)).toBe("2025-12-01");
|
||||
expect(end.toISOString().slice(0, 10)).toBe("2026-01-04");
|
||||
});
|
||||
new Date('2025-12-01T00:00:00.000Z')
|
||||
)
|
||||
expect(start.toISOString().slice(0, 10)).toBe('2025-12-01')
|
||||
expect(end.toISOString().slice(0, 10)).toBe('2026-01-04')
|
||||
})
|
||||
|
||||
it("Feb 2025 (short month): end Sunday 2025-03-02", () => {
|
||||
it('Feb 2025 (short month): end Sunday 2025-03-02', () => {
|
||||
const { start, end } = getCalendarRange(
|
||||
new Date("2025-02-01T00:00:00.000Z")
|
||||
);
|
||||
expect(start.toISOString().slice(0, 10)).toBe("2025-01-27");
|
||||
expect(end.toISOString().slice(0, 10)).toBe("2025-03-02");
|
||||
});
|
||||
new Date('2025-02-01T00:00:00.000Z')
|
||||
)
|
||||
expect(start.toISOString().slice(0, 10)).toBe('2025-01-27')
|
||||
expect(end.toISOString().slice(0, 10)).toBe('2025-03-02')
|
||||
})
|
||||
|
||||
it("sets boundary times correctly (start 00:00, end 23:59)", () => {
|
||||
it('sets boundary times correctly (start 00:00, end 23:59)', () => {
|
||||
const { start, end } = getCalendarRange(
|
||||
new Date("2025-11-01T00:00:00.000Z")
|
||||
);
|
||||
expect(start.getHours()).toBe(0);
|
||||
expect(start.getMinutes()).toBe(0);
|
||||
expect(start.getSeconds()).toBe(0);
|
||||
expect(end.getHours()).toBe(23);
|
||||
expect(end.getMinutes()).toBe(59);
|
||||
});
|
||||
});
|
||||
new Date('2025-11-01T00:00:00.000Z')
|
||||
)
|
||||
expect(start.getHours()).toBe(0)
|
||||
expect(start.getMinutes()).toBe(0)
|
||||
expect(start.getSeconds()).toBe(0)
|
||||
expect(end.getHours()).toBe(23)
|
||||
expect(end.getMinutes()).toBe(59)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,99 +1,99 @@
|
||||
import { RootState } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { findCalendarById } from "@/utils";
|
||||
import { RootState } from '@/app/store'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { findCalendarById } from '@/utils'
|
||||
|
||||
describe("findCalendarById", () => {
|
||||
describe('findCalendarById', () => {
|
||||
const mockCalendar1: Calendar = {
|
||||
id: "cal1",
|
||||
name: "Personal",
|
||||
color: { light: "#FF0000" },
|
||||
} as unknown as Calendar;
|
||||
id: 'cal1',
|
||||
name: 'Personal',
|
||||
color: { light: '#FF0000' }
|
||||
} as unknown as Calendar
|
||||
|
||||
const mockCalendar2: Calendar = {
|
||||
id: "cal2",
|
||||
name: "Work",
|
||||
color: { light: "#0000FF" },
|
||||
} as unknown as Calendar;
|
||||
id: 'cal2',
|
||||
name: 'Work',
|
||||
color: { light: '#0000FF' }
|
||||
} as unknown as Calendar
|
||||
|
||||
const mockTempCalendar: Calendar = {
|
||||
id: "temp1",
|
||||
name: "Temporary",
|
||||
color: { light: "#00FF00" },
|
||||
} as unknown as Calendar;
|
||||
id: 'temp1',
|
||||
name: 'Temporary',
|
||||
color: { light: '#00FF00' }
|
||||
} as unknown as Calendar
|
||||
|
||||
const mockState = {
|
||||
calendars: {
|
||||
list: {
|
||||
cal1: mockCalendar1,
|
||||
cal2: mockCalendar2,
|
||||
cal2: mockCalendar2
|
||||
},
|
||||
templist: {
|
||||
temp1: mockTempCalendar,
|
||||
},
|
||||
},
|
||||
} as unknown as Partial<RootState>;
|
||||
temp1: mockTempCalendar
|
||||
}
|
||||
}
|
||||
} as unknown as Partial<RootState>
|
||||
|
||||
it("should find calendar in main list", () => {
|
||||
const result = findCalendarById(mockState, "cal1");
|
||||
it('should find calendar in main list', () => {
|
||||
const result = findCalendarById(mockState, 'cal1')
|
||||
|
||||
expect(result?.calendar).toEqual(mockCalendar1);
|
||||
expect(result?.type).toBeUndefined();
|
||||
});
|
||||
expect(result?.calendar).toEqual(mockCalendar1)
|
||||
expect(result?.type).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should find calendar in temp list", () => {
|
||||
const result = findCalendarById(mockState, "temp1");
|
||||
it('should find calendar in temp list', () => {
|
||||
const result = findCalendarById(mockState, 'temp1')
|
||||
|
||||
expect(result?.calendar).toEqual(mockTempCalendar);
|
||||
expect(result?.type).toBe("temp");
|
||||
});
|
||||
expect(result?.calendar).toEqual(mockTempCalendar)
|
||||
expect(result?.type).toBe('temp')
|
||||
})
|
||||
|
||||
it("should not return calendar for non-existent id", () => {
|
||||
const result = findCalendarById(mockState, "nonexistent");
|
||||
it('should not return calendar for non-existent id', () => {
|
||||
const result = findCalendarById(mockState, 'nonexistent')
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should not return calendar for empty string", () => {
|
||||
const result = findCalendarById(mockState, "");
|
||||
it('should not return calendar for empty string', () => {
|
||||
const result = findCalendarById(mockState, '')
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should prioritize temp list over main list", () => {
|
||||
it('should prioritize temp list over main list', () => {
|
||||
const stateWithDuplicate = {
|
||||
calendars: {
|
||||
list: {
|
||||
dup1: mockCalendar1,
|
||||
dup1: mockCalendar1
|
||||
},
|
||||
templist: {
|
||||
dup1: mockTempCalendar,
|
||||
},
|
||||
},
|
||||
} as unknown as Partial<RootState>;
|
||||
dup1: mockTempCalendar
|
||||
}
|
||||
}
|
||||
} as unknown as Partial<RootState>
|
||||
|
||||
const result = findCalendarById(stateWithDuplicate, "dup1");
|
||||
const result = findCalendarById(stateWithDuplicate, 'dup1')
|
||||
|
||||
expect(result?.calendar).toEqual(mockTempCalendar);
|
||||
expect(result?.type).toBe("temp");
|
||||
});
|
||||
expect(result?.calendar).toEqual(mockTempCalendar)
|
||||
expect(result?.type).toBe('temp')
|
||||
})
|
||||
|
||||
it("should handle undefined list or templist", () => {
|
||||
it('should handle undefined list or templist', () => {
|
||||
const stateWithPartialCalendars = {
|
||||
calendars: {
|
||||
list: undefined,
|
||||
templist: { temp1: mockTempCalendar },
|
||||
},
|
||||
} as unknown as Partial<RootState>;
|
||||
templist: { temp1: mockTempCalendar }
|
||||
}
|
||||
} as unknown as Partial<RootState>
|
||||
|
||||
const result = findCalendarById(stateWithPartialCalendars, "temp1");
|
||||
expect(result?.calendar).toEqual(mockTempCalendar);
|
||||
});
|
||||
const result = findCalendarById(stateWithPartialCalendars, 'temp1')
|
||||
expect(result?.calendar).toEqual(mockTempCalendar)
|
||||
})
|
||||
|
||||
it("should handle missing calendars state", () => {
|
||||
const emptyState = {};
|
||||
it('should handle missing calendars state', () => {
|
||||
const emptyState = {}
|
||||
|
||||
const result = findCalendarById(emptyState, "cal1");
|
||||
const result = findCalendarById(emptyState, 'cal1')
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,92 +1,92 @@
|
||||
import { userData } from "@/features/User/userDataTypes";
|
||||
import { getUserDisplayName } from "@/utils/userUtils";
|
||||
import { userData } from '@/features/User/userDataTypes'
|
||||
import { getUserDisplayName } from '@/utils/userUtils'
|
||||
|
||||
describe("userUtils", () => {
|
||||
describe("getUserDisplayName", () => {
|
||||
it("returns full name when user has name and family_name", () => {
|
||||
describe('userUtils', () => {
|
||||
describe('getUserDisplayName', () => {
|
||||
it('returns full name when user has name and family_name', () => {
|
||||
const user: userData = {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
given_name: "John",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
};
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
given_name: 'John',
|
||||
name: 'John',
|
||||
sid: 'mockSid'
|
||||
}
|
||||
|
||||
expect(getUserDisplayName(user)).toBe("John Doe");
|
||||
});
|
||||
expect(getUserDisplayName(user)).toBe('John Doe')
|
||||
})
|
||||
|
||||
it("returns email when user does not have name and family_name", () => {
|
||||
it('returns email when user does not have name and family_name', () => {
|
||||
const user: userData = {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "",
|
||||
given_name: "",
|
||||
name: "",
|
||||
sid: "mockSid",
|
||||
};
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: '',
|
||||
given_name: '',
|
||||
name: '',
|
||||
sid: 'mockSid'
|
||||
}
|
||||
|
||||
expect(getUserDisplayName(user)).toBe("test@test.com");
|
||||
});
|
||||
expect(getUserDisplayName(user)).toBe('test@test.com')
|
||||
})
|
||||
|
||||
it("returns email when user has only email", () => {
|
||||
it('returns email when user has only email', () => {
|
||||
const user: userData = {
|
||||
sub: "test",
|
||||
email: "user@example.com",
|
||||
family_name: "",
|
||||
given_name: "",
|
||||
name: "",
|
||||
sid: "mockSid",
|
||||
};
|
||||
sub: 'test',
|
||||
email: 'user@example.com',
|
||||
family_name: '',
|
||||
given_name: '',
|
||||
name: '',
|
||||
sid: 'mockSid'
|
||||
}
|
||||
|
||||
expect(getUserDisplayName(user)).toBe("user@example.com");
|
||||
});
|
||||
expect(getUserDisplayName(user)).toBe('user@example.com')
|
||||
})
|
||||
|
||||
it("returns empty string when user is null", () => {
|
||||
expect(getUserDisplayName(null)).toBe("");
|
||||
});
|
||||
it('returns empty string when user is null', () => {
|
||||
expect(getUserDisplayName(null)).toBe('')
|
||||
})
|
||||
|
||||
it("returns empty string when user is undefined", () => {
|
||||
expect(getUserDisplayName(undefined)).toBe("");
|
||||
});
|
||||
it('returns empty string when user is undefined', () => {
|
||||
expect(getUserDisplayName(undefined)).toBe('')
|
||||
})
|
||||
|
||||
it("returns empty string when user has no name, family_name, or email", () => {
|
||||
it('returns empty string when user has no name, family_name, or email', () => {
|
||||
const user: userData = {
|
||||
sub: "test",
|
||||
email: "",
|
||||
family_name: "",
|
||||
given_name: "",
|
||||
name: "",
|
||||
sid: "mockSid",
|
||||
};
|
||||
sub: 'test',
|
||||
email: '',
|
||||
family_name: '',
|
||||
given_name: '',
|
||||
name: '',
|
||||
sid: 'mockSid'
|
||||
}
|
||||
|
||||
expect(getUserDisplayName(user)).toBe("");
|
||||
});
|
||||
expect(getUserDisplayName(user)).toBe('')
|
||||
})
|
||||
|
||||
it("handles user with only name (no family_name)", () => {
|
||||
it('handles user with only name (no family_name)', () => {
|
||||
const user: userData = {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "",
|
||||
given_name: "John",
|
||||
name: "John",
|
||||
sid: "mockSid",
|
||||
};
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: '',
|
||||
given_name: 'John',
|
||||
name: 'John',
|
||||
sid: 'mockSid'
|
||||
}
|
||||
|
||||
expect(getUserDisplayName(user)).toBe("test@test.com");
|
||||
});
|
||||
expect(getUserDisplayName(user)).toBe('test@test.com')
|
||||
})
|
||||
|
||||
it("handles user with only family_name (no name)", () => {
|
||||
it('handles user with only family_name (no name)', () => {
|
||||
const user: userData = {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
family_name: "Doe",
|
||||
given_name: "",
|
||||
name: "",
|
||||
sid: "mockSid",
|
||||
};
|
||||
sub: 'test',
|
||||
email: 'test@test.com',
|
||||
family_name: 'Doe',
|
||||
given_name: '',
|
||||
name: '',
|
||||
sid: 'mockSid'
|
||||
}
|
||||
|
||||
expect(getUserDisplayName(user)).toBe("test@test.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(getUserDisplayName(user)).toBe('test@test.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
["@babel/preset-react", { runtime: "automatic", importSource: "preact" }],
|
||||
],
|
||||
};
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
['@babel/preset-react', { runtime: 'automatic', importSource: 'preact' }]
|
||||
]
|
||||
}
|
||||
|
||||
+65
-40
@@ -1,74 +1,99 @@
|
||||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import react from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import importPlugin from "eslint-plugin-import";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import prettier from "eslint-config-prettier";
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import tanstackQuery from '@tanstack/eslint-plugin-query'
|
||||
import cozyReact from 'eslint-config-cozy-app/react'
|
||||
|
||||
// Flatten cozyReact config (may be an object or array)
|
||||
const cozyReactConfigs = Array.isArray(cozyReact) ? cozyReact : [cozyReact]
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["dist", "build", "node_modules"],
|
||||
ignores: [
|
||||
'dist/',
|
||||
'build/',
|
||||
'node_modules/',
|
||||
'coverage/',
|
||||
'public/',
|
||||
'*.config.js',
|
||||
'*.config.ts',
|
||||
'fileTransformer.ts'
|
||||
]
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// TanStack Query recommended rules
|
||||
...tanstackQuery.configs['flat/recommended'],
|
||||
|
||||
// Cozy React recommended rules
|
||||
...cozyReactConfigs,
|
||||
|
||||
{
|
||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: { jsx: true },
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
react,
|
||||
"react-hooks": reactHooks,
|
||||
import: importPlugin,
|
||||
jest,
|
||||
project: true
|
||||
}
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
|
||||
rules: {
|
||||
/* React */
|
||||
"react/react-in-jsx-scope": "off", // React 18+
|
||||
"react/prop-types": "off",
|
||||
'react/react-in-jsx-scope': 'off', // React 18+
|
||||
'react/prop-types': 'off',
|
||||
|
||||
/* Hooks */
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
|
||||
/* Imports */
|
||||
"import/order": "off",
|
||||
'import/order': 'off',
|
||||
|
||||
/* TS */
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_" },
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_' }
|
||||
],
|
||||
|
||||
/* Jest */
|
||||
"jest/no-disabled-tests": "warn",
|
||||
"jest/no-focused-tests": "error",
|
||||
// TO DO: Turn these back on warning
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/no-misused-promises': 'warn',
|
||||
'@typescript-eslint/no-dynamic-delete': 'warn',
|
||||
'@typescript-eslint/await-thenable': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
|
||||
/* Prettier compatibility */
|
||||
...prettier.rules,
|
||||
/* Jest */
|
||||
'jest/no-disabled-tests': 'warn',
|
||||
'jest/no-focused-tests': 'error',
|
||||
|
||||
/* Keep cozyReact prettier options but skip singleQuote/semi to avoid unnecessary changes */
|
||||
'prettier/prettier': 'error',
|
||||
|
||||
/* No noises */
|
||||
"no-debugger": "error",
|
||||
"no-console": ["warn", { allow: ["info", "warn", "error"] }],
|
||||
},
|
||||
},
|
||||
];
|
||||
'no-debugger': 'error',
|
||||
'no-console': ['warn', { allow: ['info', 'warn', 'error'] }]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
const path = require("path");
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
process(sourceText, sourcePath, options) {
|
||||
return {
|
||||
code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`,
|
||||
};
|
||||
},
|
||||
};
|
||||
code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-49
@@ -1,78 +1,78 @@
|
||||
import type { Config } from "jest";
|
||||
import type { Config } from 'jest'
|
||||
|
||||
// Set timezone to UTC for consistent test results across all environments
|
||||
process.env.TZ = "UTC";
|
||||
process.env.TZ = 'UTC'
|
||||
|
||||
const config: Config = {
|
||||
collectCoverage: true,
|
||||
coverageDirectory: "coverage",
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
projects: [
|
||||
{
|
||||
displayName: "dom",
|
||||
displayName: 'dom',
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: [
|
||||
"js",
|
||||
"mjs",
|
||||
"cjs",
|
||||
"jsx",
|
||||
"ts",
|
||||
"tsx",
|
||||
"json",
|
||||
"node",
|
||||
'js',
|
||||
'mjs',
|
||||
'cjs',
|
||||
'jsx',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json',
|
||||
'node'
|
||||
],
|
||||
testEnvironment: "jsdom",
|
||||
testMatch: ["**/*.test.tsx"],
|
||||
testEnvironment: 'jsdom',
|
||||
testMatch: ['**/*.test.tsx'],
|
||||
testTimeout: 15000,
|
||||
transform: {
|
||||
"^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.json" }],
|
||||
"^.+\\.(js|jsx|mjs)$": "babel-jest",
|
||||
"^.+\\.(css|scss|sass|less|styl|stylus)$":
|
||||
"jest-preview/transforms/css",
|
||||
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)":
|
||||
"jest-preview/transforms/file",
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/fileTransformer.ts",
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
||||
'^.+\\.(js|jsx|mjs)$': 'babel-jest',
|
||||
'^.+\\.(css|scss|sass|less|styl|stylus)$':
|
||||
'jest-preview/transforms/css',
|
||||
'^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)':
|
||||
'jest-preview/transforms/file',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
||||
'<rootDir>/fileTransformer.ts'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!(preact|@fullcalendar|react-calendar|get-user-locale|memoize|mimic-function|@wojtekmaj|ky|cozy-ui|p-map|@linagora/twake-mui|@lottiefiles)/)",
|
||||
'/node_modules/(?!(preact|@fullcalendar|react-calendar|get-user-locale|memoize|mimic-function|@wojtekmaj|ky|cozy-ui|p-map|@linagora/twake-mui|@lottiefiles)/)'
|
||||
],
|
||||
|
||||
moduleNameMapper: {
|
||||
"^preact(/(.*)|$)": "preact$1",
|
||||
"^react$": "<rootDir>/node_modules/react",
|
||||
"^react-dom$": "<rootDir>/node_modules/react-dom",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
'^preact(/(.*)|$)': 'preact$1',
|
||||
'^react$': '<rootDir>/node_modules/react',
|
||||
'^react-dom$': '<rootDir>/node_modules/react-dom',
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
},
|
||||
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts']
|
||||
},
|
||||
{
|
||||
displayName: "node",
|
||||
displayName: 'node',
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: [
|
||||
"js",
|
||||
"mjs",
|
||||
"cjs",
|
||||
"jsx",
|
||||
"ts",
|
||||
"tsx",
|
||||
"json",
|
||||
"node",
|
||||
'js',
|
||||
'mjs',
|
||||
'cjs',
|
||||
'jsx',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json',
|
||||
'node'
|
||||
],
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/*.test.ts"],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testTimeout: 15000,
|
||||
transform: {
|
||||
"^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.json" }],
|
||||
"^.+\\.(js|jsx|mjs)$": "babel-jest",
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
||||
'^.+\\.(js|jsx|mjs)$': 'babel-jest'
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/(?!(ky|@linagora/twake-mui)/)"],
|
||||
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||
transformIgnorePatterns: ['/node_modules/(?!(ky|@linagora/twake-mui)/)'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
||||
Generated
+236
-244
@@ -24,6 +24,7 @@
|
||||
"@mui/x-date-pickers": "^8.14.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"cozy-external-bridge": "^1.2.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"i18next": "^23.7.9",
|
||||
"ical.js": "^2.2.0",
|
||||
@@ -50,6 +51,7 @@
|
||||
"@rsbuild/plugin-react": "^1.4.2",
|
||||
"@rsbuild/plugin-stylus": "^1.2.0",
|
||||
"@rsbuild/plugin-svgr": "^1.2.3",
|
||||
"@tanstack/eslint-plugin-query": "^5.95.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -63,13 +65,9 @@
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"babel-jest": "^30.2.0",
|
||||
"babel-plugin-istanbul": "^7.0.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest": "^29.13.0",
|
||||
"eslint": "10.0.2",
|
||||
"eslint-config-cozy-app": "7.0.0",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"history": "^5.3.0",
|
||||
"i18next-resources-for-ts": "1.4.0",
|
||||
"jest": "^30.2.0",
|
||||
@@ -2332,198 +2330,111 @@
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"node_modules/@eslint/compat": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.2.tgz",
|
||||
"integrity": "sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"@eslint/core": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.40 || 9 || 10"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
|
||||
"integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^3.0.3",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
"minimatch": "^10.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
|
||||
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
|
||||
"integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.17.0"
|
||||
"@eslint/core": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
|
||||
"integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz",
|
||||
"integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.14.0",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^10.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.3",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
|
||||
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz",
|
||||
"integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
|
||||
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^10.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
|
||||
"integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
|
||||
"integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/core": "^1.1.1",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/core": {
|
||||
@@ -4333,6 +4244,29 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query": {
|
||||
"version": "5.95.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.95.2.tgz",
|
||||
"integrity": "sha512-EYUFRaqjBep4EHMPpZR12sXP7Kr5qv9iDIlq93NfbhHwhITaW6Txu3ROO6dLFz5r84T8p+oZXBG77pa2Wuok7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.48.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
@@ -4516,6 +4450,13 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/esrecurse": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -6751,19 +6692,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
@@ -7365,33 +7300,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz",
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz",
|
||||
"integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.3",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@eslint/config-array": "^0.23.2",
|
||||
"@eslint/config-helpers": "^0.5.2",
|
||||
"@eslint/core": "^1.1.0",
|
||||
"@eslint/plugin-kit": "^0.6.0",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"ajv": "^6.14.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^8.4.0",
|
||||
"eslint-visitor-keys": "^4.2.1",
|
||||
"espree": "^10.4.0",
|
||||
"esquery": "^1.5.0",
|
||||
"eslint-scope": "^9.1.1",
|
||||
"eslint-visitor-keys": "^5.0.1",
|
||||
"espree": "^11.1.1",
|
||||
"esquery": "^1.7.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^8.0.0",
|
||||
@@ -7401,8 +7333,7 @@
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^10.2.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -7410,7 +7341,7 @@
|
||||
"eslint": "bin/eslint.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
@@ -7424,6 +7355,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-cozy-app": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-cozy-app/-/eslint-config-cozy-app-7.0.0.tgz",
|
||||
"integrity": "sha512-NphYDGxEfzQPSQr2RNivCKhxqJviJ5HPtKcjkPWjVlV/OFuaaIwgGF3QPQ6qlDKRxayY/JK3zMbGuf4U2j9DSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint/compat": "2.0.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jest": "29.15.0",
|
||||
"eslint-plugin-prettier": "5.5.5",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"globals": "17.4.0",
|
||||
"prettier": "3.8.1",
|
||||
"typescript-eslint": "8.56.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^10.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||
@@ -7595,6 +7556,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
|
||||
"integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.1",
|
||||
"synckit": "^0.11.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-plugin-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/eslint": ">=8.0.0",
|
||||
"eslint": ">=8.0.0",
|
||||
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
|
||||
"prettier": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/eslint": {
|
||||
"optional": true
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-promise": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz",
|
||||
@@ -7756,49 +7748,33 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-scope": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@types/esrecurse": "^4.3.1",
|
||||
"@types/estree": "^1.0.8",
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -7847,19 +7823,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
|
||||
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
@@ -7877,31 +7840,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
||||
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
"eslint-visitor-keys": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/espree/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -8046,6 +8009,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -8579,9 +8549,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||
"version": "17.4.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
|
||||
"integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -11193,13 +11163,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -11363,13 +11326,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz",
|
||||
"integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -12261,6 +12224,19 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-linter-helpers": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
|
||||
"integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
@@ -14104,6 +14080,22 @@
|
||||
"react-dom": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/twake-i18n/node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
+8
-6
@@ -19,6 +19,7 @@
|
||||
"@mui/x-date-pickers": "^8.14.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"cozy-external-bridge": "^1.2.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"i18next": "^23.7.9",
|
||||
"ical.js": "^2.2.0",
|
||||
@@ -69,6 +70,7 @@
|
||||
"@rsbuild/plugin-react": "^1.4.2",
|
||||
"@rsbuild/plugin-stylus": "^1.2.0",
|
||||
"@rsbuild/plugin-svgr": "^1.2.3",
|
||||
"@tanstack/eslint-plugin-query": "^5.95.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -82,13 +84,9 @@
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"babel-jest": "^30.2.0",
|
||||
"babel-plugin-istanbul": "^7.0.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest": "^29.13.0",
|
||||
"eslint": "10.0.2",
|
||||
"eslint-config-cozy-app": "7.0.0",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"history": "^5.3.0",
|
||||
"i18next-resources-for-ts": "1.4.0",
|
||||
"jest": "^30.2.0",
|
||||
@@ -99,5 +97,9 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-eslint": "^8.54.0"
|
||||
},
|
||||
"overrides": {
|
||||
"eslint": "$eslint",
|
||||
"typescript": "$typescript"
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -1,18 +1,18 @@
|
||||
var SSO_BASE_URL = "https://example.com";
|
||||
var SSO_CLIENT_ID = "example";
|
||||
var SSO_SCOPE = "openid profile email";
|
||||
var SSO_REDIRECT_URI = "https://example.com/callback";
|
||||
var SSO_RESPONSE_TYPE = "code";
|
||||
var SSO_CODE_CHALLENGE_METHOD = "S256";
|
||||
var SSO_POST_LOGOUT_REDIRECT = "http://example.com?logout=1";
|
||||
var CALENDAR_BASE_URL = "https://calendar.example.com";
|
||||
var DAV_BASE_URL = "https://dav.example.com";
|
||||
var MAIL_SPA_URL = "https://mail.example.com";
|
||||
var VIDEO_CONFERENCE_BASE_URL = "https://meet.linagora.com";
|
||||
var SUPPORT_URL = "https://twake.app/support/";
|
||||
var DEBUG = false;
|
||||
var LANG = "en";
|
||||
var WEBSOCKET_URL = "wss://calendar.example.com";
|
||||
var WS_DEBOUNCE_PERIOD_MS = 100; // milliseconds, remove or set to 0 to disable debounce
|
||||
var WS_PING_PERIOD_MS = 30000;
|
||||
var WS_PING_TIMEOUT_PERIOD_MS = 35000;
|
||||
var SSO_BASE_URL = 'https://example.com'
|
||||
var SSO_CLIENT_ID = 'example'
|
||||
var SSO_SCOPE = 'openid profile email'
|
||||
var SSO_REDIRECT_URI = 'https://example.com/callback'
|
||||
var SSO_RESPONSE_TYPE = 'code'
|
||||
var SSO_CODE_CHALLENGE_METHOD = 'S256'
|
||||
var SSO_POST_LOGOUT_REDIRECT = 'http://example.com?logout=1'
|
||||
var CALENDAR_BASE_URL = 'https://calendar.example.com'
|
||||
var DAV_BASE_URL = 'https://dav.example.com'
|
||||
var MAIL_SPA_URL = 'https://mail.example.com'
|
||||
var VIDEO_CONFERENCE_BASE_URL = 'https://meet.linagora.com'
|
||||
var SUPPORT_URL = 'https://twake.app/support/'
|
||||
var DEBUG = false
|
||||
var LANG = 'en'
|
||||
var WEBSOCKET_URL = 'wss://calendar.example.com'
|
||||
var WS_DEBOUNCE_PERIOD_MS = 100 // milliseconds, remove or set to 0 to disable debounce
|
||||
var WS_PING_PERIOD_MS = 30000
|
||||
var WS_PING_TIMEOUT_PERIOD_MS = 35000
|
||||
|
||||
+11
-11
@@ -1,17 +1,17 @@
|
||||
var appList = [
|
||||
{
|
||||
name: "Chat",
|
||||
link: "/twake",
|
||||
icon: "/assets/images/svg/app-chat.svg",
|
||||
name: 'Chat',
|
||||
link: '/twake',
|
||||
icon: '/assets/images/svg/app-chat.svg'
|
||||
},
|
||||
{
|
||||
name: "Drive",
|
||||
link: "/drive",
|
||||
icon: "/assets/images/svg/app-drive.svg",
|
||||
name: 'Drive',
|
||||
link: '/drive',
|
||||
icon: '/assets/images/svg/app-drive.svg'
|
||||
},
|
||||
{
|
||||
name: "Mail",
|
||||
link: "/mail",
|
||||
icon: "/assets/images/svg/app-mail.svg",
|
||||
},
|
||||
];
|
||||
name: 'Mail',
|
||||
link: '/mail',
|
||||
icon: '/assets/images/svg/app-mail.svg'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("InterVariable.ttf") format("truetype");
|
||||
font-family: 'Inter';
|
||||
src: url('InterVariable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("Roboto-VariableFont_wdth,wght.ttf") format("truetype");
|
||||
font-family: 'Roboto';
|
||||
src: url('Roboto-VariableFont_wdth,wght.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Cal Sans";
|
||||
src: url("CalSans-SemiBold.woff2") format("woff2");
|
||||
font-family: 'Cal Sans';
|
||||
src: url('CalSans-SemiBold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
||||
+15
-15
@@ -1,32 +1,32 @@
|
||||
import { defineConfig } from "@rsbuild/core";
|
||||
import { pluginReact } from "@rsbuild/plugin-react";
|
||||
import { pluginStylus } from "@rsbuild/plugin-stylus";
|
||||
import { pluginSvgr } from "@rsbuild/plugin-svgr";
|
||||
import { defineConfig } from '@rsbuild/core'
|
||||
import { pluginReact } from '@rsbuild/plugin-react'
|
||||
import { pluginStylus } from '@rsbuild/plugin-stylus'
|
||||
import { pluginSvgr } from '@rsbuild/plugin-svgr'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [pluginReact(), pluginStylus(), pluginSvgr()],
|
||||
html: {
|
||||
template: "./public/index.html",
|
||||
template: './public/index.html'
|
||||
},
|
||||
server: {
|
||||
port: 5000,
|
||||
historyApiFallback: true,
|
||||
historyApiFallback: true
|
||||
},
|
||||
source: {
|
||||
entry: {
|
||||
index: "./src/index.tsx",
|
||||
},
|
||||
index: './src/index.tsx'
|
||||
}
|
||||
},
|
||||
output: {
|
||||
distPath: {
|
||||
root: "dist",
|
||||
root: 'dist'
|
||||
},
|
||||
minify: false,
|
||||
minify: false
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
react: require.resolve("react"),
|
||||
"react-dom": require.resolve("react-dom"),
|
||||
},
|
||||
},
|
||||
});
|
||||
react: require.resolve('react'),
|
||||
'react-dom': require.resolve('react-dom')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+55
-55
@@ -1,84 +1,84 @@
|
||||
import { TwakeMuiThemeProvider } from "@linagora/twake-mui";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { push } from "redux-first-history";
|
||||
import { HistoryRouter as Router } from "redux-first-history/rr6";
|
||||
import "./App.styl";
|
||||
import { useAppDispatch, useAppSelector } from "./app/hooks";
|
||||
import { history } from "./app/store";
|
||||
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
||||
import { Error as ErrorPage } from "./components/Error/Error";
|
||||
import { ErrorSnackbar } from "./components/Error/ErrorSnackbar";
|
||||
import { Loading } from "./components/Loading/Loading";
|
||||
import { AVAILABLE_LANGUAGES } from "./features/Settings/constants";
|
||||
import HandleLogin from "./features/User/HandleLogin";
|
||||
import { CallbackResume } from "./features/User/LoginCallback";
|
||||
import { useInitializeApp } from "./features/User/useInitializeApp";
|
||||
import { ScreenTooSmall } from "./ScreenTooSmall";
|
||||
import { WebSocketGate } from "./websocket/WebSocketGate";
|
||||
import { TwakeMuiThemeProvider } from '@linagora/twake-mui'
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { push } from 'redux-first-history'
|
||||
import { HistoryRouter as Router } from 'redux-first-history/rr6'
|
||||
import './App.styl'
|
||||
import { useAppDispatch, useAppSelector } from './app/hooks'
|
||||
import { history } from './app/store'
|
||||
import CalendarLayout from './components/Calendar/CalendarLayout'
|
||||
import { Error as ErrorPage } from './components/Error/Error'
|
||||
import { ErrorSnackbar } from './components/Error/ErrorSnackbar'
|
||||
import { Loading } from './components/Loading/Loading'
|
||||
import { AVAILABLE_LANGUAGES } from './features/Settings/constants'
|
||||
import HandleLogin from './features/User/HandleLogin'
|
||||
import { CallbackResume } from './features/User/LoginCallback'
|
||||
import { useInitializeApp } from './features/User/useInitializeApp'
|
||||
import { ScreenTooSmall } from './ScreenTooSmall'
|
||||
import { WebSocketGate } from './websocket/WebSocketGate'
|
||||
|
||||
import {
|
||||
enGB,
|
||||
fr as frLocale,
|
||||
ru as ruLocale,
|
||||
vi as viLocale,
|
||||
} from "date-fns/locale";
|
||||
vi as viLocale
|
||||
} from 'date-fns/locale'
|
||||
|
||||
import I18n from "twake-i18n";
|
||||
import en from "./locales/en.json";
|
||||
import fr from "./locales/fr.json";
|
||||
import ru from "./locales/ru.json";
|
||||
import vi from "./locales/vi.json";
|
||||
import I18n from 'twake-i18n'
|
||||
import en from './locales/en.json'
|
||||
import fr from './locales/fr.json'
|
||||
import ru from './locales/ru.json'
|
||||
import vi from './locales/vi.json'
|
||||
|
||||
const locale = { en, fr, ru, vi };
|
||||
const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale };
|
||||
const locale = { en, fr, ru, vi }
|
||||
const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale }
|
||||
|
||||
const SUPPORTED_LANGUAGES = AVAILABLE_LANGUAGES.map((lang) => lang.code);
|
||||
type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
||||
const SUPPORTED_LANGUAGES = AVAILABLE_LANGUAGES.map(lang => lang.code)
|
||||
type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]
|
||||
|
||||
const isValidLanguage = (
|
||||
lang: string | null | undefined
|
||||
): lang is SupportedLanguage => {
|
||||
return !!lang && SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage);
|
||||
};
|
||||
return !!lang && SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const error = useAppSelector((state) => state.user.error);
|
||||
const appLoading = useAppSelector((state) => state.loading.isLoading);
|
||||
const userLanguage = useAppSelector(
|
||||
(state) => state.user.coreConfig.language
|
||||
);
|
||||
const settingsLanguage = useAppSelector((state) => state.settings.language);
|
||||
const savedLang = localStorage.getItem("lang");
|
||||
const defaultLang = window.LANG;
|
||||
const error = useAppSelector(state => state.user.error)
|
||||
const appLoading = useAppSelector(state => state.loading.isLoading)
|
||||
const userLanguage = useAppSelector(state => state.user.coreConfig.language)
|
||||
const settingsLanguage = useAppSelector(state => state.settings.language)
|
||||
const savedLang = localStorage.getItem('lang')
|
||||
const defaultLang = window.LANG
|
||||
|
||||
const lang =
|
||||
[userLanguage, settingsLanguage, savedLang, defaultLang].find(
|
||||
(l) => !!l && isValidLanguage(l)
|
||||
) || "en";
|
||||
l => !!l && isValidLanguage(l)
|
||||
) || 'en'
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const dispatch = useAppDispatch()
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
dispatch(push("/error"));
|
||||
dispatch(push('/error'))
|
||||
}
|
||||
}, [error, dispatch]);
|
||||
}, [error, dispatch])
|
||||
|
||||
const SMALL_SCREEN_QUERY = "(max-width: 925px)";
|
||||
const SMALL_SCREEN_QUERY = '(max-width: 925px)'
|
||||
const [isTooSmall, setIsTooSmall] = useState(
|
||||
() => window.matchMedia(SMALL_SCREEN_QUERY).matches
|
||||
);
|
||||
)
|
||||
|
||||
useInitializeApp();
|
||||
useInitializeApp()
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(SMALL_SCREEN_QUERY);
|
||||
const mediaQuery = window.matchMedia(SMALL_SCREEN_QUERY)
|
||||
const onChange = (event: MediaQueryListEvent) =>
|
||||
setIsTooSmall(event.matches);
|
||||
setIsTooSmall(mediaQuery.matches);
|
||||
mediaQuery.addEventListener("change", onChange);
|
||||
return () => mediaQuery.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
setIsTooSmall(event.matches)
|
||||
const onChangeInitial = () => setIsTooSmall(mediaQuery.matches)
|
||||
|
||||
onChangeInitial()
|
||||
mediaQuery.addEventListener('change', onChange)
|
||||
return () => mediaQuery.removeEventListener('change', onChange)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TwakeMuiThemeProvider>
|
||||
@@ -108,7 +108,7 @@ function App() {
|
||||
)}
|
||||
</I18n>
|
||||
</TwakeMuiThemeProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
||||
Vendored
+7
-7
@@ -1,11 +1,11 @@
|
||||
declare namespace Intl {
|
||||
type Key =
|
||||
| "calendar"
|
||||
| "collation"
|
||||
| "currency"
|
||||
| "numberingSystem"
|
||||
| "timeZone"
|
||||
| "unit";
|
||||
| 'calendar'
|
||||
| 'collation'
|
||||
| 'currency'
|
||||
| 'numberingSystem'
|
||||
| 'timeZone'
|
||||
| 'unit'
|
||||
|
||||
function supportedValuesOf(input: Key): string[];
|
||||
function supportedValuesOf(input: Key): string[]
|
||||
}
|
||||
|
||||
+15
-15
@@ -1,32 +1,32 @@
|
||||
import calendarImage from "@/static/images/calendar.svg";
|
||||
import { Box, Typography } from "@linagora/twake-mui";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import calendarImage from '@/static/images/calendar.svg'
|
||||
import { Box, Typography } from '@linagora/twake-mui'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
|
||||
export function ScreenTooSmall() {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
gap: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<img src={calendarImage} style={{ width: 80, height: 80 }} alt="logo" />
|
||||
|
||||
<Typography variant="h6">{t("mobile.comingSoon.title")}</Typography>
|
||||
<Typography variant="h6">{t('mobile.comingSoon.title')}</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: "264px" }}
|
||||
sx={{ maxWidth: '264px' }}
|
||||
>
|
||||
{t("mobile.comingSoon.subtitle")}
|
||||
{t('mobile.comingSoon.subtitle')}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { AppDispatch, RootState } from "./store";
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { AppDispatch, RootState } from './store'
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
|
||||
+14
-14
@@ -1,30 +1,30 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
interface LoadingState {
|
||||
isLoading: boolean;
|
||||
message?: string;
|
||||
isLoading: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
const initialState: LoadingState = {
|
||||
isLoading: false,
|
||||
message: undefined,
|
||||
};
|
||||
message: undefined
|
||||
}
|
||||
|
||||
const loadingSlice = createSlice({
|
||||
name: "loading",
|
||||
name: 'loading',
|
||||
initialState,
|
||||
reducers: {
|
||||
setAppLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
state.isLoading = action.payload
|
||||
if (!action.payload) {
|
||||
state.message = undefined;
|
||||
state.message = undefined
|
||||
}
|
||||
},
|
||||
setLoadingMessage: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.message = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
state.message = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { setAppLoading, setLoadingMessage } = loadingSlice.actions;
|
||||
export default loadingSlice.reducer;
|
||||
export const { setAppLoading, setLoadingMessage } = loadingSlice.actions
|
||||
export default loadingSlice.reducer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RootState } from "@/app/store";
|
||||
import { createSelector } from "@reduxjs/toolkit";
|
||||
import { RootState } from '@/app/store'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
|
||||
export const selectCalendars = createSelector(
|
||||
(state: RootState) => state.calendars.list,
|
||||
(list) => Object.values(list)
|
||||
);
|
||||
list => Object.values(list)
|
||||
)
|
||||
|
||||
+20
-20
@@ -1,14 +1,14 @@
|
||||
import eventsCalendar from "@/features/Calendars/CalendarSlice";
|
||||
import searchResultReducer from "@/features/Search/SearchSlice";
|
||||
import settingsReducer from "@/features/Settings/SettingsSlice";
|
||||
import userReducer from "@/features/User/userSlice";
|
||||
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||
import loadingReducer from "./loadingSlice";
|
||||
import { createBrowserHistory } from "history";
|
||||
import { createReduxHistoryContext } from "redux-first-history";
|
||||
import eventsCalendar from '@/features/Calendars/CalendarSlice'
|
||||
import searchResultReducer from '@/features/Search/SearchSlice'
|
||||
import settingsReducer from '@/features/Settings/SettingsSlice'
|
||||
import userReducer from '@/features/User/userSlice'
|
||||
import { combineReducers, configureStore } from '@reduxjs/toolkit'
|
||||
import loadingReducer from './loadingSlice'
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { createReduxHistoryContext } from 'redux-first-history'
|
||||
|
||||
const { createReduxHistory, routerMiddleware, routerReducer } =
|
||||
createReduxHistoryContext({ history: createBrowserHistory() });
|
||||
createReduxHistoryContext({ history: createBrowserHistory() })
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
router: routerReducer,
|
||||
@@ -16,22 +16,22 @@ const rootReducer = combineReducers({
|
||||
calendars: eventsCalendar,
|
||||
settings: settingsReducer,
|
||||
searchResult: searchResultReducer,
|
||||
loading: loadingReducer,
|
||||
});
|
||||
loading: loadingReducer
|
||||
})
|
||||
|
||||
export const setupStore = (preloadedState?: Partial<RootState>) => {
|
||||
return configureStore({
|
||||
reducer: rootReducer,
|
||||
preloadedState,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(routerMiddleware),
|
||||
});
|
||||
};
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware().concat(routerMiddleware)
|
||||
})
|
||||
}
|
||||
|
||||
export const store = setupStore();
|
||||
export const store = setupStore()
|
||||
|
||||
export const history = createReduxHistory(store);
|
||||
export const history = createReduxHistory(store)
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
export type AppStore = ReturnType<typeof setupStore>;
|
||||
export type AppDispatch = AppStore["dispatch"];
|
||||
export type RootState = ReturnType<typeof rootReducer>
|
||||
export type AppStore = ReturnType<typeof setupStore>
|
||||
export type AppDispatch = AppStore['dispatch']
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { createAttendee } from "@/features/User/models/attendee.mapper";
|
||||
import { useRef, useState } from "react";
|
||||
import { FreeBusyIndicator } from "./FreeBusyIndicator";
|
||||
import { userAttendee } from '@/features/User/models/attendee'
|
||||
import { createAttendee } from '@/features/User/models/attendee.mapper'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { FreeBusyIndicator } from './FreeBusyIndicator'
|
||||
import {
|
||||
ExtendedAutocompleteRenderInputParams,
|
||||
PeopleSearch,
|
||||
User,
|
||||
} from "./PeopleSearch";
|
||||
import { FreeBusyMap, useAttendeesFreeBusy } from "./useFreeBusy";
|
||||
User
|
||||
} from './PeopleSearch'
|
||||
import { FreeBusyMap, useAttendeesFreeBusy } from './useFreeBusy'
|
||||
|
||||
const attendeeToUser = (a: userAttendee, openpaasId = ""): User => ({
|
||||
const attendeeToUser = (a: userAttendee, openpaasId = ''): User => ({
|
||||
email: a.cal_address,
|
||||
displayName: a.cn ?? "",
|
||||
avatarUrl: "",
|
||||
openpaasId,
|
||||
});
|
||||
displayName: a.cn ?? '',
|
||||
avatarUrl: '',
|
||||
openpaasId
|
||||
})
|
||||
|
||||
const hasCalendar = (u: User) => u.objectType === "user" && !!u.openpaasId;
|
||||
const hasCalendar = (u: User) => u.objectType === 'user' && !!u.openpaasId
|
||||
|
||||
export default function AttendeeSearch({
|
||||
attendees,
|
||||
@@ -27,98 +27,109 @@ export default function AttendeeSearch({
|
||||
start,
|
||||
end,
|
||||
timezone,
|
||||
eventUid,
|
||||
eventUid
|
||||
}: {
|
||||
attendees: userAttendee[];
|
||||
setAttendees: (attendees: userAttendee[]) => void;
|
||||
disabled?: boolean;
|
||||
inputSlot?: (
|
||||
params: ExtendedAutocompleteRenderInputParams
|
||||
) => React.ReactNode;
|
||||
placeholder?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
timezone?: string;
|
||||
eventUid?: string | null;
|
||||
attendees: userAttendee[]
|
||||
setAttendees: (attendees: userAttendee[]) => void
|
||||
disabled?: boolean
|
||||
inputSlot?: (params: ExtendedAutocompleteRenderInputParams) => React.ReactNode
|
||||
placeholder?: string
|
||||
start?: string
|
||||
end?: string
|
||||
timezone?: string
|
||||
eventUid?: string | null
|
||||
}) {
|
||||
const [userIdMap, setUserIdMap] = useState<Record<string, string>>({});
|
||||
const [addedUsers, setAddedUsers] = useState<User[]>([]);
|
||||
const initialEmailsRef = useRef<Set<string> | null>(null);
|
||||
if (initialEmailsRef.current === null && !!eventUid && attendees.length > 0) {
|
||||
initialEmailsRef.current = new Set(attendees.map((a) => a.cal_address));
|
||||
}
|
||||
const initialEmails = eventUid
|
||||
? (initialEmailsRef.current ?? new Set<string>())
|
||||
: new Set<string>();
|
||||
const [userIdMap, setUserIdMap] = useState<Record<string, string>>({})
|
||||
const [addedUsers, setAddedUsers] = useState<User[]>([])
|
||||
const initialEmailsRef = useRef<Set<string> | null>(null)
|
||||
const [initialEmailsSet, setInitialEmailsSet] = useState<Set<string>>(
|
||||
new Set()
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const updateInitialEmailsSet = () => {
|
||||
if (
|
||||
initialEmailsRef.current === null &&
|
||||
!!eventUid &&
|
||||
attendees.length > 0
|
||||
) {
|
||||
initialEmailsRef.current = new Set(attendees.map(a => a.cal_address))
|
||||
setInitialEmailsSet(initialEmailsRef.current)
|
||||
}
|
||||
}
|
||||
updateInitialEmailsSet()
|
||||
}, [eventUid, attendees])
|
||||
|
||||
const initialEmails = eventUid ? initialEmailsSet : new Set<string>()
|
||||
|
||||
const selectedUsers: User[] = [
|
||||
...addedUsers,
|
||||
...attendees
|
||||
.map((a) => attendeeToUser(a, userIdMap[a.cal_address]))
|
||||
.filter((a) => !addedUsers.find((u) => a.email === u.email)),
|
||||
];
|
||||
.map(a => attendeeToUser(a, userIdMap[a.cal_address]))
|
||||
.filter(a => !addedUsers.find(u => a.email === u.email))
|
||||
]
|
||||
|
||||
const toAttendee = (u: User) => ({
|
||||
email: u.email,
|
||||
userId: u.openpaasId || userIdMap[u.email] || null,
|
||||
});
|
||||
userId: u.openpaasId || userIdMap[u.email] || null
|
||||
})
|
||||
|
||||
const existingAttendees = selectedUsers
|
||||
.filter((u) => initialEmails.has(u.email))
|
||||
.map(toAttendee);
|
||||
.filter(u => initialEmails.has(u.email))
|
||||
.map(toAttendee)
|
||||
const newAttendees = selectedUsers
|
||||
.filter((u) => !initialEmails.has(u.email) && hasCalendar(u))
|
||||
.map(toAttendee);
|
||||
.filter(u => !initialEmails.has(u.email) && hasCalendar(u))
|
||||
.map(toAttendee)
|
||||
|
||||
// Contacts and freeSolo users get a static "contact" status — no API call needed
|
||||
const contactMap: FreeBusyMap = Object.fromEntries(
|
||||
selectedUsers
|
||||
.filter((u) => !initialEmails.has(u.email) && !hasCalendar(u))
|
||||
.map((u) => [u.email, "contact" as const])
|
||||
);
|
||||
.filter(u => !initialEmails.has(u.email) && !hasCalendar(u))
|
||||
.map(u => [u.email, 'contact' as const])
|
||||
)
|
||||
|
||||
const freeBusyMap = useAttendeesFreeBusy({
|
||||
existingAttendees,
|
||||
newAttendees,
|
||||
start: start ?? "",
|
||||
end: end ?? "",
|
||||
timezone: timezone ?? "",
|
||||
start: start ?? '',
|
||||
end: end ?? '',
|
||||
timezone: timezone ?? '',
|
||||
eventUid,
|
||||
enabled: !!(start && end && selectedUsers.length > 0),
|
||||
});
|
||||
enabled: !!(start && end && selectedUsers.length > 0)
|
||||
})
|
||||
|
||||
const statusMap = { ...freeBusyMap, ...contactMap };
|
||||
const statusMap = { ...freeBusyMap, ...contactMap }
|
||||
|
||||
return (
|
||||
<PeopleSearch
|
||||
selectedUsers={selectedUsers}
|
||||
objectTypes={["user", "contact"]}
|
||||
objectTypes={['user', 'contact']}
|
||||
disabled={disabled}
|
||||
inputSlot={inputSlot}
|
||||
placeholder={placeholder}
|
||||
getChipIcon={
|
||||
start && end
|
||||
? (user) => (
|
||||
<FreeBusyIndicator status={statusMap[user.email] ?? "unknown"} />
|
||||
? user => (
|
||||
<FreeBusyIndicator status={statusMap[user.email] ?? 'unknown'} />
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onChange={(_event, value: User[]) => {
|
||||
setUserIdMap((prev) => {
|
||||
const next = { ...prev };
|
||||
setUserIdMap(prev => {
|
||||
const next = { ...prev }
|
||||
for (const u of value) {
|
||||
if (u.openpaasId && u.email) next[u.email] = u.openpaasId;
|
||||
if (u.openpaasId && u.email) next[u.email] = u.openpaasId
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setAddedUsers(value.filter((u) => !initialEmails.has(u.email)));
|
||||
return next
|
||||
})
|
||||
setAddedUsers(value.filter(u => !initialEmails.has(u.email)))
|
||||
setAttendees(
|
||||
value.map((u) =>
|
||||
value.map(u =>
|
||||
createAttendee({ cal_address: u.email, cn: u.displayName })
|
||||
)
|
||||
);
|
||||
)
|
||||
}}
|
||||
freeSolo
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
import { Tooltip } from "@linagora/twake-mui";
|
||||
import AccessTimeFilledIcon from "@mui/icons-material/AccessTimeFilled";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { FreeBusyStatus } from "./useFreeBusy";
|
||||
import { Tooltip } from '@linagora/twake-mui'
|
||||
import AccessTimeFilledIcon from '@mui/icons-material/AccessTimeFilled'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { FreeBusyStatus } from './useFreeBusy'
|
||||
|
||||
interface FreeBusyIndicatorProps {
|
||||
status: FreeBusyStatus;
|
||||
size?: number;
|
||||
status: FreeBusyStatus
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function FreeBusyIndicator({ status }: FreeBusyIndicatorProps) {
|
||||
const { t } = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useI18n()
|
||||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
if (status === "busy") setOpen(true);
|
||||
}, [status]);
|
||||
const triggerOpen = () => {
|
||||
if (status === 'busy') setOpen(true)
|
||||
}
|
||||
triggerOpen()
|
||||
}, [status])
|
||||
|
||||
if (status !== "busy") return null;
|
||||
if (status !== 'busy') return null
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
{t("event.freeBusy.busy")}
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{t('event.freeBusy.busy')}
|
||||
<CloseIcon
|
||||
fontSize="inherit"
|
||||
style={{ cursor: "pointer" }}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
</span>
|
||||
@@ -35,16 +38,16 @@ export function FreeBusyIndicator({ status }: FreeBusyIndicatorProps) {
|
||||
disableHoverListener
|
||||
placement="bottom-start"
|
||||
onClose={() => setOpen(false)}
|
||||
slotProps={{ tooltip: { sx: { opacity: 1, bgcolor: "grey.900" } } }}
|
||||
slotProps={{ tooltip: { sx: { opacity: 1, bgcolor: 'grey.900' } } }}
|
||||
>
|
||||
<AccessTimeFilledIcon
|
||||
aria-label={t("event.freeBusy.busy")}
|
||||
aria-label={t('event.freeBusy.busy')}
|
||||
color="warning"
|
||||
style={{
|
||||
margin: "0 -6px 0 5px",
|
||||
flexShrink: 0,
|
||||
margin: '0 -6px 0 5px',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||
import { stringAvatar } from "@/components/Event/utils/eventUtils";
|
||||
import { useUserSearch } from "./useUserSearch";
|
||||
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { getAccessiblePair } from '@/utils/getAccessiblePair'
|
||||
import { stringAvatar } from '@/components/Event/utils/eventUtils'
|
||||
import { useUserSearch } from './useUserSearch'
|
||||
import { SnackbarAlert } from '@/components/Loading/SnackBarAlert'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import {
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
@@ -15,35 +15,35 @@ import {
|
||||
PopperProps,
|
||||
TextField,
|
||||
useTheme,
|
||||
type AutocompleteRenderInputParams,
|
||||
} from "@linagora/twake-mui";
|
||||
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
|
||||
type AutocompleteRenderInputParams
|
||||
} from '@linagora/twake-mui'
|
||||
import PeopleOutlineOutlinedIcon from '@mui/icons-material/PeopleOutlineOutlined'
|
||||
import {
|
||||
HTMLAttributes,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
type SyntheticEvent,
|
||||
} from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ResourceIcon } from "./ResourceIcon";
|
||||
import { isValidEmail } from "../../utils/isValidEmail";
|
||||
import { usePasteHandler } from "./usePasteHandler";
|
||||
type SyntheticEvent
|
||||
} from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { ResourceIcon } from './ResourceIcon'
|
||||
import { isValidEmail } from '../../utils/isValidEmail'
|
||||
import { usePasteHandler } from './usePasteHandler'
|
||||
|
||||
export interface User {
|
||||
email: string;
|
||||
displayName: string;
|
||||
avatarUrl?: string;
|
||||
openpaasId?: string;
|
||||
color?: Record<string, string>;
|
||||
objectType?: string;
|
||||
email: string
|
||||
displayName: string
|
||||
avatarUrl?: string
|
||||
openpaasId?: string
|
||||
color?: Record<string, string>
|
||||
objectType?: string
|
||||
}
|
||||
|
||||
export interface ExtendedAutocompleteRenderInputParams extends AutocompleteRenderInputParams {
|
||||
error?: boolean;
|
||||
helperText?: string | null;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
error?: boolean
|
||||
helperText?: string | null
|
||||
placeholder?: string
|
||||
label?: string
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export function PeopleSearch({
|
||||
@@ -57,33 +57,31 @@ export function PeopleSearch({
|
||||
inputSlot,
|
||||
customRenderInput,
|
||||
customSlotProps,
|
||||
getChipIcon,
|
||||
getChipIcon
|
||||
}: {
|
||||
selectedUsers: User[];
|
||||
onChange: (event: SyntheticEvent, users: User[]) => void;
|
||||
objectTypes: string[];
|
||||
disabled?: boolean;
|
||||
freeSolo?: boolean;
|
||||
onToggleEventPreview?: () => void;
|
||||
placeholder?: string;
|
||||
inputSlot?: (
|
||||
params: ExtendedAutocompleteRenderInputParams
|
||||
) => React.ReactNode;
|
||||
selectedUsers: User[]
|
||||
onChange: (event: SyntheticEvent, users: User[]) => void
|
||||
objectTypes: string[]
|
||||
disabled?: boolean
|
||||
freeSolo?: boolean
|
||||
onToggleEventPreview?: () => void
|
||||
placeholder?: string
|
||||
inputSlot?: (params: ExtendedAutocompleteRenderInputParams) => React.ReactNode
|
||||
customRenderInput?: (
|
||||
params: AutocompleteRenderInputParams,
|
||||
query: string,
|
||||
setQuery: (value: string) => void
|
||||
) => ReactNode;
|
||||
) => ReactNode
|
||||
customSlotProps?: {
|
||||
popper?: Partial<PopperProps>;
|
||||
paper?: Partial<PaperProps>;
|
||||
listbox?: Partial<HTMLAttributes<HTMLUListElement>>;
|
||||
};
|
||||
getChipIcon?: (user: User) => ReactNode;
|
||||
popper?: Partial<PopperProps>
|
||||
paper?: Partial<PaperProps>
|
||||
listbox?: Partial<HTMLAttributes<HTMLUListElement>>
|
||||
}
|
||||
getChipIcon?: (user: User) => ReactNode
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const searchPlaceholder = placeholder ?? t("peopleSearch.placeholder");
|
||||
const errorMessage = t("peopleSearch.searchError");
|
||||
const { t } = useI18n()
|
||||
const searchPlaceholder = placeholder ?? t('peopleSearch.placeholder')
|
||||
const errorMessage = t('peopleSearch.searchError')
|
||||
|
||||
const {
|
||||
query,
|
||||
@@ -98,32 +96,32 @@ export function PeopleSearch({
|
||||
snackbarOpen,
|
||||
setSnackbarOpen,
|
||||
snackbarMessage,
|
||||
setSnackbarMessage,
|
||||
} = useUserSearch<User>({ objectTypes, errorMessage });
|
||||
setSnackbarMessage
|
||||
} = useUserSearch<User>({ objectTypes, errorMessage })
|
||||
|
||||
const theme = useTheme();
|
||||
const theme = useTheme()
|
||||
|
||||
const handleBlurCommit = useCallback(
|
||||
(event: React.SyntheticEvent) => {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
const trimmed = query.trim()
|
||||
if (!trimmed) return
|
||||
if (!isValidEmail(trimmed)) {
|
||||
setInputError(
|
||||
t("peopleSearch.invalidEmail").replace("%{email}", trimmed)
|
||||
);
|
||||
return;
|
||||
t('peopleSearch.invalidEmail').replace('%{email}', trimmed)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (selectedUsers.find((u) => u.email === trimmed)) {
|
||||
setQuery("");
|
||||
return;
|
||||
if (selectedUsers.find(u => u.email === trimmed)) {
|
||||
setQuery('')
|
||||
return
|
||||
}
|
||||
setInputError(null);
|
||||
const newUser: User = { email: trimmed, displayName: trimmed };
|
||||
onChange(event, [...selectedUsers, newUser]);
|
||||
setQuery("");
|
||||
setInputError(null)
|
||||
const newUser: User = { email: trimmed, displayName: trimmed }
|
||||
onChange(event, [...selectedUsers, newUser])
|
||||
setQuery('')
|
||||
},
|
||||
[query, selectedUsers, onChange, t, setInputError, setQuery]
|
||||
);
|
||||
)
|
||||
|
||||
const handlePaste = usePasteHandler({
|
||||
freeSolo,
|
||||
@@ -131,8 +129,8 @@ export function PeopleSearch({
|
||||
onChange,
|
||||
setQuery,
|
||||
setInputError,
|
||||
t,
|
||||
});
|
||||
t
|
||||
})
|
||||
|
||||
const defaultRenderInput = useCallback(
|
||||
(params: AutocompleteRenderInputParams) => {
|
||||
@@ -143,7 +141,7 @@ export function PeopleSearch({
|
||||
) : (
|
||||
<PeopleOutlineOutlinedIcon
|
||||
fontSize="small"
|
||||
sx={{ mr: 1, color: "action.active" }}
|
||||
sx={{ mr: 1, color: 'action.active' }}
|
||||
/>
|
||||
),
|
||||
endAdornment: (
|
||||
@@ -151,61 +149,61 @@ export function PeopleSearch({
|
||||
{loading ? <CircularProgress color="inherit" size={20} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
),
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const enhancedParams = {
|
||||
...params,
|
||||
InputProps: inputProps,
|
||||
inputProps: {
|
||||
...params.inputProps,
|
||||
autoComplete: "off",
|
||||
onPaste: handlePaste,
|
||||
},
|
||||
};
|
||||
autoComplete: 'off',
|
||||
onPaste: handlePaste
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && onToggleEventPreview) {
|
||||
e.preventDefault();
|
||||
onToggleEventPreview();
|
||||
if (e.key === 'Enter' && onToggleEventPreview) {
|
||||
e.preventDefault()
|
||||
onToggleEventPreview()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const defaultTextFieldProps = {
|
||||
error: !!inputError,
|
||||
helperText: inputError,
|
||||
placeholder: searchPlaceholder,
|
||||
label: "",
|
||||
label: '',
|
||||
onKeyDown: handleEnterKey,
|
||||
slotProps: {
|
||||
input: {
|
||||
...inputProps,
|
||||
},
|
||||
},
|
||||
};
|
||||
...inputProps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputSlot) {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={params.id} className="visually-hidden">
|
||||
{t("peopleSearch.label")}
|
||||
{t('peopleSearch.label')}
|
||||
</label>
|
||||
{inputSlot({
|
||||
...enhancedParams,
|
||||
error: !!inputError,
|
||||
helperText: inputError,
|
||||
placeholder: searchPlaceholder,
|
||||
label: "",
|
||||
onKeyDown: handleEnterKey,
|
||||
label: '',
|
||||
onKeyDown: handleEnterKey
|
||||
})}
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={params.id} className="visually-hidden">
|
||||
{t("peopleSearch.label")}
|
||||
{t('peopleSearch.label')}
|
||||
</label>
|
||||
<TextField
|
||||
{...enhancedParams}
|
||||
@@ -214,7 +212,7 @@ export function PeopleSearch({
|
||||
size="medium"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@@ -223,9 +221,9 @@ export function PeopleSearch({
|
||||
onToggleEventPreview,
|
||||
loading,
|
||||
searchPlaceholder,
|
||||
handlePaste,
|
||||
handlePaste
|
||||
]
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -246,68 +244,68 @@ export function PeopleSearch({
|
||||
onClose={() => setIsOpen(false)}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
filterOptions={(x) => x}
|
||||
filterOptions={x => x}
|
||||
fullWidth
|
||||
noOptionsText={t("peopleSearch.noResults")}
|
||||
loadingText={t("peopleSearch.loading")}
|
||||
getOptionLabel={(option) => {
|
||||
if (typeof option === "object") {
|
||||
return option.displayName || option.email;
|
||||
noOptionsText={t('peopleSearch.noResults')}
|
||||
loadingText={t('peopleSearch.loading')}
|
||||
getOptionLabel={option => {
|
||||
if (typeof option === 'object') {
|
||||
return option.displayName || option.email
|
||||
} else {
|
||||
return option;
|
||||
return option
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiAutocomplete-inputRoot": {
|
||||
py: 0,
|
||||
},
|
||||
'& .MuiAutocomplete-inputRoot': {
|
||||
py: 0
|
||||
}
|
||||
}}
|
||||
filterSelectedOptions
|
||||
value={selectedUsers}
|
||||
inputValue={query}
|
||||
onInputChange={(_event, value) => setQuery(value)}
|
||||
onChange={(event, value) => {
|
||||
const last = value[value.length - 1];
|
||||
if (typeof last === "string" && !isValidEmail(last.trim())) {
|
||||
const invalidEmailMessage = t("peopleSearch.invalidEmail").replace(
|
||||
"%{email}",
|
||||
const last = value[value.length - 1]
|
||||
if (typeof last === 'string' && !isValidEmail(last.trim())) {
|
||||
const invalidEmailMessage = t('peopleSearch.invalidEmail').replace(
|
||||
'%{email}',
|
||||
last
|
||||
);
|
||||
setInputError(invalidEmailMessage);
|
||||
return;
|
||||
)
|
||||
setInputError(invalidEmailMessage)
|
||||
return
|
||||
}
|
||||
setInputError(null);
|
||||
setInputError(null)
|
||||
const mapped = value
|
||||
.map((v: string | User) =>
|
||||
typeof v === "string"
|
||||
typeof v === 'string'
|
||||
? { email: v.trim(), displayName: v.trim() }
|
||||
: v
|
||||
)
|
||||
.filter(
|
||||
(user, index, self) =>
|
||||
self.findIndex((u) => u.email === user.email) === index
|
||||
);
|
||||
onChange(event, mapped);
|
||||
self.findIndex(u => u.email === user.email) === index
|
||||
)
|
||||
onChange(event, mapped)
|
||||
}}
|
||||
slotProps={{
|
||||
...customSlotProps,
|
||||
popper: {
|
||||
placement: "bottom-start",
|
||||
sx: { minWidth: "300px", ...customSlotProps?.popper?.sx },
|
||||
...customSlotProps?.popper,
|
||||
},
|
||||
placement: 'bottom-start',
|
||||
sx: { minWidth: '300px', ...customSlotProps?.popper?.sx },
|
||||
...customSlotProps?.popper
|
||||
}
|
||||
}}
|
||||
forcePopupIcon={false}
|
||||
disableClearable
|
||||
renderInput={(params) =>
|
||||
renderInput={params =>
|
||||
customRenderInput
|
||||
? customRenderInput(params, query, setQuery)
|
||||
: defaultRenderInput(params)
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
if (selectedUsers.find((u) => u.email === option.email)) return null;
|
||||
const { key, ...otherProps } = props;
|
||||
const isResource = option.objectType === "resource";
|
||||
if (selectedUsers.find(u => u.email === option.email)) return null
|
||||
const { key, ...otherProps } = props
|
||||
const isResource = option.objectType === 'resource'
|
||||
return (
|
||||
<ListItem key={key + option?.email} {...otherProps} disableGutters>
|
||||
<ListItemAvatar>
|
||||
@@ -321,25 +319,23 @@ export function PeopleSearch({
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={option.displayName}
|
||||
secondary={isResource ? "" : option.email}
|
||||
secondary={isResource ? '' : option.email}
|
||||
slotProps={{
|
||||
primary: { variant: "body2" },
|
||||
secondary: { variant: "caption" },
|
||||
primary: { variant: 'body2' },
|
||||
secondary: { variant: 'caption' }
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
)
|
||||
}}
|
||||
renderValue={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const isString = typeof option === "string";
|
||||
const label = isString
|
||||
? option
|
||||
: option.displayName || option.email;
|
||||
const isString = typeof option === 'string'
|
||||
const label = isString ? option : option.displayName || option.email
|
||||
const chipColor = isString
|
||||
? theme.palette.grey[200]
|
||||
: (option.color?.light ?? theme.palette.grey[200]);
|
||||
const textColor = getAccessiblePair(chipColor, theme);
|
||||
: (option.color?.light ?? theme.palette.grey[200])
|
||||
const textColor = getAccessiblePair(chipColor, theme)
|
||||
|
||||
return (
|
||||
<Chip
|
||||
@@ -351,25 +347,25 @@ export function PeopleSearch({
|
||||
deleteIcon={<CloseIcon />}
|
||||
style={{
|
||||
backgroundColor: chipColor,
|
||||
color: textColor,
|
||||
color: textColor
|
||||
}}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
)
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SnackbarAlert
|
||||
open={snackbarOpen}
|
||||
setOpen={(open: boolean) => {
|
||||
setSnackbarOpen(open);
|
||||
setSnackbarOpen(open)
|
||||
if (!open) {
|
||||
setSnackbarMessage("");
|
||||
setSnackbarMessage('')
|
||||
}
|
||||
}}
|
||||
message={snackbarMessage}
|
||||
severity="error"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { Avatar, Box } from "@linagora/twake-mui";
|
||||
import LayersOutlinedIcon from "@mui/icons-material/LayersOutlined";
|
||||
import { Avatar, Box } from '@linagora/twake-mui'
|
||||
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined'
|
||||
|
||||
interface ResourceIconProps {
|
||||
avatarUrl?: string;
|
||||
colorIcon?: boolean;
|
||||
color?: string;
|
||||
avatarUrl?: string
|
||||
colorIcon?: boolean
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function ResourceIcon({
|
||||
avatarUrl,
|
||||
colorIcon,
|
||||
color,
|
||||
color
|
||||
}: ResourceIconProps) {
|
||||
if (colorIcon && avatarUrl) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
backgroundColor: color,
|
||||
maskImage: `url(${avatarUrl})`,
|
||||
maskSize: "cover",
|
||||
maskSize: 'cover'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return avatarUrl ? (
|
||||
<Avatar
|
||||
sx={{ backgroundColor: "transparent", width: "24px", height: "24px" }}
|
||||
sx={{ backgroundColor: 'transparent', width: '24px', height: '24px' }}
|
||||
src={avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
sx={{ backgroundColor: "transparent", width: "24px", height: "24px" }}
|
||||
sx={{ backgroundColor: 'transparent', width: '24px', height: '24px' }}
|
||||
>
|
||||
<LayersOutlinedIcon />
|
||||
</Avatar>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||
import { useUserSearch } from "./useUserSearch";
|
||||
import { ResourceIcon } from "./ResourceIcon";
|
||||
import { SnackbarAlert } from "@/components/Loading/SnackBarAlert";
|
||||
import { getAccessiblePair } from '@/utils/getAccessiblePair'
|
||||
import { useUserSearch } from './useUserSearch'
|
||||
import { ResourceIcon } from './ResourceIcon'
|
||||
import { SnackbarAlert } from '@/components/Loading/SnackBarAlert'
|
||||
import {
|
||||
Autocomplete,
|
||||
Chip,
|
||||
@@ -14,31 +14,31 @@ import {
|
||||
TextField,
|
||||
useTheme,
|
||||
Typography,
|
||||
type AutocompleteRenderInputParams,
|
||||
} from "@linagora/twake-mui";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
type AutocompleteRenderInputParams
|
||||
} from '@linagora/twake-mui'
|
||||
import SearchIcon from '@mui/icons-material/Search'
|
||||
import {
|
||||
HTMLAttributes,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
type SyntheticEvent,
|
||||
} from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
type SyntheticEvent
|
||||
} from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
|
||||
export interface Resource {
|
||||
email?: string;
|
||||
displayName: string;
|
||||
avatarUrl?: string;
|
||||
openpaasId?: string;
|
||||
color?: Record<string, string>;
|
||||
email?: string
|
||||
displayName: string
|
||||
avatarUrl?: string
|
||||
openpaasId?: string
|
||||
color?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ExtendedAutocompleteRenderInputParams extends AutocompleteRenderInputParams {
|
||||
error?: boolean;
|
||||
helperText?: string | null;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
error?: boolean
|
||||
helperText?: string | null
|
||||
placeholder?: string
|
||||
label?: string
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export function ResourceSearch({
|
||||
@@ -52,33 +52,31 @@ export function ResourceSearch({
|
||||
inputSlot,
|
||||
customRenderInput,
|
||||
customSlotProps,
|
||||
hideLabel,
|
||||
hideLabel
|
||||
}: {
|
||||
selectedResources: Resource[];
|
||||
onChange: (event: SyntheticEvent, users: Resource[]) => void;
|
||||
objectTypes: string[];
|
||||
disabled?: boolean;
|
||||
freeSolo?: boolean;
|
||||
onToggleEventPreview?: () => void;
|
||||
placeholder?: string;
|
||||
inputSlot?: (
|
||||
params: ExtendedAutocompleteRenderInputParams
|
||||
) => React.ReactNode;
|
||||
selectedResources: Resource[]
|
||||
onChange: (event: SyntheticEvent, users: Resource[]) => void
|
||||
objectTypes: string[]
|
||||
disabled?: boolean
|
||||
freeSolo?: boolean
|
||||
onToggleEventPreview?: () => void
|
||||
placeholder?: string
|
||||
inputSlot?: (params: ExtendedAutocompleteRenderInputParams) => React.ReactNode
|
||||
customRenderInput?: (
|
||||
params: AutocompleteRenderInputParams,
|
||||
query: string,
|
||||
setQuery: (value: string) => void
|
||||
) => ReactNode;
|
||||
) => ReactNode
|
||||
customSlotProps?: {
|
||||
popper?: Partial<PopperProps>;
|
||||
paper?: Partial<PaperProps>;
|
||||
listbox?: Partial<HTMLAttributes<HTMLUListElement>>;
|
||||
};
|
||||
hideLabel?: boolean;
|
||||
popper?: Partial<PopperProps>
|
||||
paper?: Partial<PaperProps>
|
||||
listbox?: Partial<HTMLAttributes<HTMLUListElement>>
|
||||
}
|
||||
hideLabel?: boolean
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const searchPlaceholder = placeholder ?? t("resourceSearch.placeholder");
|
||||
const errorMessage = t("resourceSearch.searchError");
|
||||
const { t } = useI18n()
|
||||
const searchPlaceholder = placeholder ?? t('resourceSearch.placeholder')
|
||||
const errorMessage = t('resourceSearch.searchError')
|
||||
|
||||
const {
|
||||
query,
|
||||
@@ -93,26 +91,26 @@ export function ResourceSearch({
|
||||
snackbarOpen,
|
||||
setSnackbarOpen,
|
||||
snackbarMessage,
|
||||
setSnackbarMessage,
|
||||
} = useUserSearch<Resource>({ objectTypes, errorMessage });
|
||||
setSnackbarMessage
|
||||
} = useUserSearch<Resource>({ objectTypes, errorMessage })
|
||||
|
||||
const theme = useTheme();
|
||||
const theme = useTheme()
|
||||
|
||||
const handleBlurCommit = useCallback(
|
||||
(event: React.SyntheticEvent) => {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
if (selectedResources.find((u) => u.displayName === trimmed)) {
|
||||
setQuery("");
|
||||
return;
|
||||
const trimmed = query.trim()
|
||||
if (!trimmed) return
|
||||
if (selectedResources.find(u => u.displayName === trimmed)) {
|
||||
setQuery('')
|
||||
return
|
||||
}
|
||||
setInputError(null);
|
||||
const newResource: Resource = { displayName: trimmed };
|
||||
onChange(event, [...selectedResources, newResource]);
|
||||
setQuery("");
|
||||
setInputError(null)
|
||||
const newResource: Resource = { displayName: trimmed }
|
||||
onChange(event, [...selectedResources, newResource])
|
||||
setQuery('')
|
||||
},
|
||||
[query, selectedResources, onChange, setInputError, setQuery]
|
||||
);
|
||||
)
|
||||
|
||||
const defaultRenderInput = useCallback(
|
||||
(params: AutocompleteRenderInputParams) => {
|
||||
@@ -123,7 +121,7 @@ export function ResourceSearch({
|
||||
{!selectedResources?.length ? (
|
||||
<SearchIcon
|
||||
fontSize="small"
|
||||
sx={{ mr: 1, color: "action.active" }}
|
||||
sx={{ mr: 1, color: 'action.active' }}
|
||||
/>
|
||||
) : null}
|
||||
{params.InputProps.startAdornment}
|
||||
@@ -134,44 +132,44 @@ export function ResourceSearch({
|
||||
{loading ? <CircularProgress color="inherit" size={20} /> : null}
|
||||
{!selectedResources?.length ? params.InputProps.endAdornment : null}
|
||||
</>
|
||||
),
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const enhancedParams = {
|
||||
...params,
|
||||
InputProps: inputProps,
|
||||
inputProps: {
|
||||
...params.inputProps,
|
||||
autoComplete: "off",
|
||||
},
|
||||
};
|
||||
autoComplete: 'off'
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && onToggleEventPreview) {
|
||||
e.preventDefault();
|
||||
onToggleEventPreview();
|
||||
if (e.key === 'Enter' && onToggleEventPreview) {
|
||||
e.preventDefault()
|
||||
onToggleEventPreview()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const defaultTextFieldProps = {
|
||||
error: !!inputError,
|
||||
helperText: inputError,
|
||||
placeholder: searchPlaceholder,
|
||||
label: "",
|
||||
label: '',
|
||||
onKeyDown: handleEnterKey,
|
||||
slotProps: {
|
||||
input: {
|
||||
...inputProps,
|
||||
},
|
||||
},
|
||||
};
|
||||
...inputProps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputSlot) {
|
||||
return (
|
||||
<>
|
||||
{!hideLabel && (
|
||||
<Typography variant="h6" sx={{ marginBottom: "10px" }}>
|
||||
{t("resourceSearch.label")}
|
||||
<Typography variant="h6" sx={{ marginBottom: '10px' }}>
|
||||
{t('resourceSearch.label')}
|
||||
</Typography>
|
||||
)}
|
||||
{inputSlot({
|
||||
@@ -179,18 +177,18 @@ export function ResourceSearch({
|
||||
error: !!inputError,
|
||||
helperText: inputError,
|
||||
placeholder: searchPlaceholder,
|
||||
label: "",
|
||||
onKeyDown: handleEnterKey,
|
||||
label: '',
|
||||
onKeyDown: handleEnterKey
|
||||
})}
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideLabel && (
|
||||
<Typography variant="h6" sx={{ marginBottom: "10px" }}>
|
||||
{t("resourceSearch.label")}
|
||||
<Typography variant="h6" sx={{ marginBottom: '10px' }}>
|
||||
{t('resourceSearch.label')}
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
@@ -200,7 +198,7 @@ export function ResourceSearch({
|
||||
size="medium"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@@ -209,9 +207,9 @@ export function ResourceSearch({
|
||||
onToggleEventPreview,
|
||||
loading,
|
||||
searchPlaceholder,
|
||||
selectedResources?.length,
|
||||
selectedResources?.length
|
||||
]
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -234,55 +232,53 @@ export function ResourceSearch({
|
||||
loading={loading}
|
||||
filterOptions={(options: Resource[]) => options}
|
||||
fullWidth
|
||||
noOptionsText={t("resourceSearch.noResults")}
|
||||
loadingText={t("resourceSearch.loading")}
|
||||
noOptionsText={t('resourceSearch.noResults')}
|
||||
loadingText={t('resourceSearch.loading')}
|
||||
getOptionLabel={(option: Resource | string) => {
|
||||
if (typeof option === "object") {
|
||||
return option.displayName;
|
||||
if (typeof option === 'object') {
|
||||
return option.displayName
|
||||
} else {
|
||||
return option;
|
||||
return option
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiAutocomplete-inputRoot": {
|
||||
py: 0,
|
||||
},
|
||||
'& .MuiAutocomplete-inputRoot': {
|
||||
py: 0
|
||||
}
|
||||
}}
|
||||
filterSelectedOptions
|
||||
value={selectedResources}
|
||||
inputValue={query}
|
||||
onInputChange={(_event, value: string) => setQuery(value)}
|
||||
onChange={(event, value: string[] | Resource[]) => {
|
||||
setInputError(null);
|
||||
setInputError(null)
|
||||
const mapped = value
|
||||
.map((v: string | Resource) =>
|
||||
typeof v === "string" ? { displayName: v.trim() } : v
|
||||
typeof v === 'string' ? { displayName: v.trim() } : v
|
||||
)
|
||||
.filter((v) => v.displayName.trim().length > 0);
|
||||
onChange(event, mapped);
|
||||
.filter(v => v.displayName.trim().length > 0)
|
||||
onChange(event, mapped)
|
||||
}}
|
||||
slotProps={{
|
||||
...customSlotProps,
|
||||
popper: {
|
||||
placement: "bottom-start",
|
||||
sx: { minWidth: "300px", ...customSlotProps?.popper?.sx },
|
||||
...customSlotProps?.popper,
|
||||
},
|
||||
placement: 'bottom-start',
|
||||
sx: { minWidth: '300px', ...customSlotProps?.popper?.sx },
|
||||
...customSlotProps?.popper
|
||||
}
|
||||
}}
|
||||
// When render input is custom, the adornments should be handled by the custom component
|
||||
forcePopupIcon={!customRenderInput}
|
||||
disableClearable={!!customRenderInput}
|
||||
renderInput={(params) =>
|
||||
renderInput={params =>
|
||||
customRenderInput
|
||||
? customRenderInput(params, query, setQuery)
|
||||
: defaultRenderInput(params)
|
||||
}
|
||||
renderOption={(props, option: Resource) => {
|
||||
if (
|
||||
selectedResources.find((u) => u.displayName === option.displayName)
|
||||
)
|
||||
return null;
|
||||
const { key, ...otherProps } = props;
|
||||
if (selectedResources.find(u => u.displayName === option.displayName))
|
||||
return null
|
||||
const { key, ...otherProps } = props
|
||||
return (
|
||||
<ListItem
|
||||
key={key + option?.displayName}
|
||||
@@ -294,16 +290,16 @@ export function ResourceSearch({
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={option.displayName} />
|
||||
</ListItem>
|
||||
);
|
||||
)
|
||||
}}
|
||||
renderValue={(value: string[] | Resource[], getTagProps) =>
|
||||
value.map((option: string | Resource, index) => {
|
||||
const isString = typeof option === "string";
|
||||
const label = isString ? option : option.displayName;
|
||||
const isString = typeof option === 'string'
|
||||
const label = isString ? option : option.displayName
|
||||
const chipColor = isString
|
||||
? theme.palette.grey[300]
|
||||
: (option.color?.light ?? theme.palette.grey[300]);
|
||||
const textColor = getAccessiblePair(chipColor, theme);
|
||||
: (option.color?.light ?? theme.palette.grey[300])
|
||||
const textColor = getAccessiblePair(chipColor, theme)
|
||||
|
||||
return (
|
||||
<Chip
|
||||
@@ -311,25 +307,25 @@ export function ResourceSearch({
|
||||
key={label}
|
||||
style={{
|
||||
backgroundColor: chipColor,
|
||||
color: textColor,
|
||||
color: textColor
|
||||
}}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
)
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SnackbarAlert
|
||||
open={snackbarOpen}
|
||||
setOpen={(open: boolean) => {
|
||||
setSnackbarOpen(open);
|
||||
setSnackbarOpen(open)
|
||||
if (!open) {
|
||||
setSnackbarMessage("");
|
||||
setSnackbarMessage('')
|
||||
}
|
||||
}}
|
||||
message={snackbarMessage}
|
||||
severity="error"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,99 +1,92 @@
|
||||
import { getFreeBusyForAddedAttendeesREPORT } from "@/features/Events/api/getFreeBusyForAddedAttendeesREPORT";
|
||||
import { getFreeBusyForEventAttendeesPOST } from "@/features/Events/api/getFreeBusyForEventAttendeesPOST";
|
||||
import { getUserDataFromEmail } from "@/features/Events/api/getUserDataFromEmail";
|
||||
import moment from "moment-timezone";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { getFreeBusyForAddedAttendeesREPORT } from '@/features/Events/api/getFreeBusyForAddedAttendeesREPORT'
|
||||
import { getFreeBusyForEventAttendeesPOST } from '@/features/Events/api/getFreeBusyForEventAttendeesPOST'
|
||||
import { getUserDataFromEmail } from '@/features/Events/api/getUserDataFromEmail'
|
||||
import moment from 'moment-timezone'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export type FreeBusyStatus =
|
||||
| "free"
|
||||
| "busy"
|
||||
| "loading"
|
||||
| "contact"
|
||||
| "unknown";
|
||||
export type FreeBusyMap = Record<string, FreeBusyStatus>;
|
||||
export type FreeBusyStatus = 'free' | 'busy' | 'loading' | 'contact' | 'unknown'
|
||||
export type FreeBusyMap = Record<string, FreeBusyStatus>
|
||||
|
||||
interface Attendee {
|
||||
email: string;
|
||||
userId?: string | null;
|
||||
email: string
|
||||
userId?: string | null
|
||||
}
|
||||
|
||||
interface ResolvedAttendee {
|
||||
email: string;
|
||||
userId: string;
|
||||
email: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
// Helpers
|
||||
async function resolveUserId(attendee: Attendee): Promise<string | null> {
|
||||
if (attendee.userId) return attendee.userId;
|
||||
if (attendee.userId) return attendee.userId
|
||||
return getUserDataFromEmail(attendee.email)
|
||||
.then((u) => u[0]?._id ?? null)
|
||||
.catch(() => null);
|
||||
.then(u => u[0]?._id ?? null)
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
async function resolveAll(attendees: Attendee[]): Promise<ResolvedAttendee[]> {
|
||||
const results = await Promise.all(
|
||||
attendees.map(async (a) => {
|
||||
const userId = await resolveUserId(a);
|
||||
return userId ? { email: a.email, userId } : null;
|
||||
attendees.map(async a => {
|
||||
const userId = await resolveUserId(a)
|
||||
return userId ? { email: a.email, userId } : null
|
||||
})
|
||||
);
|
||||
return results.filter((r): r is ResolvedAttendee => r !== null);
|
||||
)
|
||||
return results.filter((r): r is ResolvedAttendee => r !== null)
|
||||
}
|
||||
|
||||
export function hasFreeBusyConflict(data: unknown): boolean {
|
||||
try {
|
||||
const jcal = (data as { data: unknown[] }).data;
|
||||
if (!Array.isArray(jcal) || jcal[0] !== "vcalendar") return false;
|
||||
const components = jcal[2] as unknown[][];
|
||||
return (
|
||||
Array.isArray(components) && components.some(isVFreeBusyWithConflict)
|
||||
);
|
||||
const jcal = (data as { data: unknown[] }).data
|
||||
if (!Array.isArray(jcal) || jcal[0] !== 'vcalendar') return false
|
||||
const components = jcal[2] as unknown[][]
|
||||
return Array.isArray(components) && components.some(isVFreeBusyWithConflict)
|
||||
} catch {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isVFreeBusyWithConflict(component: unknown): boolean {
|
||||
if (!Array.isArray(component) || component[0] !== "vfreebusy") return false;
|
||||
const props = component[1] as unknown[][];
|
||||
if (!Array.isArray(component) || component[0] !== 'vfreebusy') return false
|
||||
const props = component[1] as unknown[][]
|
||||
return (
|
||||
Array.isArray(props) &&
|
||||
props.some((p) => Array.isArray(p) && p[0] === "freebusy")
|
||||
);
|
||||
props.some(p => Array.isArray(p) && p[0] === 'freebusy')
|
||||
)
|
||||
}
|
||||
|
||||
function toUtcIcal(datetime: string, timezone: string): string {
|
||||
return moment.tz(datetime, timezone).utc().format("YYYYMMDDTHHmmss");
|
||||
return moment.tz(datetime, timezone).utc().format('YYYYMMDDTHHmmss')
|
||||
}
|
||||
|
||||
async function fetchFreeBusyMap(
|
||||
attendees: Attendee[],
|
||||
fetcher: (resolved: ResolvedAttendee[]) => Promise<FreeBusyMap>
|
||||
): Promise<FreeBusyMap> {
|
||||
const resolved = await resolveAll(attendees);
|
||||
const resolved = await resolveAll(attendees)
|
||||
|
||||
const unresolved: FreeBusyMap = Object.fromEntries(
|
||||
attendees
|
||||
.filter((a) => !resolved.find((r) => r.email === a.email))
|
||||
.map((a) => [a.email, "unknown" as FreeBusyStatus])
|
||||
);
|
||||
.filter(a => !resolved.find(r => r.email === a.email))
|
||||
.map(a => [a.email, 'unknown' as FreeBusyStatus])
|
||||
)
|
||||
|
||||
if (resolved.length === 0) return unresolved;
|
||||
if (resolved.length === 0) return unresolved
|
||||
|
||||
const fetched = await fetcher(resolved);
|
||||
return { ...unresolved, ...fetched };
|
||||
const fetched = await fetcher(resolved)
|
||||
return { ...unresolved, ...fetched }
|
||||
}
|
||||
|
||||
function toLoadingMap(attendees: Attendee[]): FreeBusyMap {
|
||||
return Object.fromEntries(
|
||||
attendees.map((a) => [a.email, "loading" as FreeBusyStatus])
|
||||
);
|
||||
attendees.map(a => [a.email, 'loading' as FreeBusyStatus])
|
||||
)
|
||||
}
|
||||
|
||||
function toUnknownMap(attendees: Attendee[]): FreeBusyMap {
|
||||
return Object.fromEntries(
|
||||
attendees.map((a) => [a.email, "unknown" as FreeBusyStatus])
|
||||
);
|
||||
attendees.map(a => [a.email, 'unknown' as FreeBusyStatus])
|
||||
)
|
||||
}
|
||||
|
||||
function toFreeBusyMap(
|
||||
@@ -101,26 +94,26 @@ function toFreeBusyMap(
|
||||
): (busyByUserId: Record<string, boolean>) => FreeBusyMap {
|
||||
const userIdToEmail = Object.fromEntries(
|
||||
resolved.map(({ email, userId }) => [userId, email])
|
||||
);
|
||||
return (busyByUserId) =>
|
||||
)
|
||||
return busyByUserId =>
|
||||
Object.fromEntries(
|
||||
Object.entries(busyByUserId).flatMap(([uid, busy]) => {
|
||||
const email = userIdToEmail[uid];
|
||||
const email = userIdToEmail[uid]
|
||||
return email
|
||||
? [[email, (busy ? "busy" : "free") as FreeBusyStatus]]
|
||||
: [];
|
||||
? [[email, (busy ? 'busy' : 'free') as FreeBusyStatus]]
|
||||
: []
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
interface UseAttendeesFreeBusyOptions {
|
||||
existingAttendees: Attendee[];
|
||||
newAttendees: Attendee[];
|
||||
start: string;
|
||||
end: string;
|
||||
timezone: string;
|
||||
eventUid?: string | null;
|
||||
enabled?: boolean;
|
||||
existingAttendees: Attendee[]
|
||||
newAttendees: Attendee[]
|
||||
start: string
|
||||
end: string
|
||||
timezone: string
|
||||
eventUid?: string | null
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useAttendeesFreeBusy({
|
||||
@@ -130,18 +123,18 @@ export function useAttendeesFreeBusy({
|
||||
end,
|
||||
timezone,
|
||||
eventUid,
|
||||
enabled = true,
|
||||
enabled = true
|
||||
}: UseAttendeesFreeBusyOptions): FreeBusyMap {
|
||||
const [statusMap, setStatusMap] = useState<FreeBusyMap>({});
|
||||
const fetchedNewEmailsRef = useRef<Set<string>>(new Set());
|
||||
const [statusMap, setStatusMap] = useState<FreeBusyMap>({})
|
||||
const fetchedNewEmailsRef = useRef<Set<string>>(new Set())
|
||||
|
||||
const existingKey = existingAttendees.map((a) => a.email).join(",");
|
||||
const newKey = newAttendees.map((a) => a.email).join(",");
|
||||
const existingKey = existingAttendees.map(a => a.email).join(',')
|
||||
const newKey = newAttendees.map(a => a.email).join(',')
|
||||
|
||||
useEffect(() => {
|
||||
fetchedNewEmailsRef.current = new Set();
|
||||
setStatusMap({});
|
||||
}, [start, end, timezone]);
|
||||
fetchedNewEmailsRef.current = new Set()
|
||||
setStatusMap({})
|
||||
}, [start, end, timezone])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -151,101 +144,105 @@ export function useAttendeesFreeBusy({
|
||||
!eventUid ||
|
||||
existingAttendees.length === 0
|
||||
)
|
||||
return;
|
||||
return
|
||||
|
||||
let cancelled = false;
|
||||
setStatusMap((prev) => ({ ...prev, ...toLoadingMap(existingAttendees) }));
|
||||
let cancelled = false
|
||||
setStatusMap(prev => ({ ...prev, ...toLoadingMap(existingAttendees) }))
|
||||
|
||||
fetchFreeBusyMap(existingAttendees, (resolved) =>
|
||||
fetchFreeBusyMap(existingAttendees, resolved =>
|
||||
getFreeBusyForEventAttendeesPOST(
|
||||
resolved.map((r) => r.userId),
|
||||
resolved.map(r => r.userId),
|
||||
toUtcIcal(start, timezone),
|
||||
toUtcIcal(end, timezone),
|
||||
eventUid!
|
||||
eventUid
|
||||
).then(toFreeBusyMap(resolved))
|
||||
)
|
||||
.then((updates) => {
|
||||
if (!cancelled) setStatusMap((prev) => ({ ...prev, ...updates }));
|
||||
.then(updates => {
|
||||
if (!cancelled) {
|
||||
setStatusMap(prev => ({ ...prev, ...updates }))
|
||||
}
|
||||
return
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled)
|
||||
setStatusMap((prev) => ({
|
||||
setStatusMap(prev => ({
|
||||
...prev,
|
||||
...toUnknownMap(existingAttendees),
|
||||
}));
|
||||
});
|
||||
...toUnknownMap(existingAttendees)
|
||||
}))
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
cancelled = true
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [existingKey, start, end, eventUid, enabled, timezone]);
|
||||
}, [existingKey, start, end, eventUid, enabled, timezone])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !start || !end) return;
|
||||
if (!enabled || !start || !end) return
|
||||
|
||||
const currentEmails = new Set(newAttendees.map((a) => a.email));
|
||||
const currentEmails = new Set(newAttendees.map(a => a.email))
|
||||
const removedEmails = [...fetchedNewEmailsRef.current].filter(
|
||||
(e) => !currentEmails.has(e)
|
||||
);
|
||||
e => !currentEmails.has(e)
|
||||
)
|
||||
if (removedEmails.length > 0) {
|
||||
removedEmails.forEach((e) => {
|
||||
fetchedNewEmailsRef.current.delete(e);
|
||||
});
|
||||
setStatusMap((prev) => {
|
||||
const next = { ...prev };
|
||||
removedEmails.forEach((e) => {
|
||||
delete next[e];
|
||||
});
|
||||
return next;
|
||||
});
|
||||
removedEmails.forEach(e => {
|
||||
fetchedNewEmailsRef.current.delete(e)
|
||||
})
|
||||
setStatusMap(prev => {
|
||||
const next = { ...prev }
|
||||
removedEmails.forEach(e => {
|
||||
delete next[e]
|
||||
})
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const toFetch = newAttendees.filter(
|
||||
(a) => !fetchedNewEmailsRef.current.has(a.email)
|
||||
);
|
||||
if (toFetch.length === 0) return;
|
||||
a => !fetchedNewEmailsRef.current.has(a.email)
|
||||
)
|
||||
if (toFetch.length === 0) return
|
||||
|
||||
let cancelled = false;
|
||||
setStatusMap((prev) => ({ ...prev, ...toLoadingMap(toFetch) }));
|
||||
fetchFreeBusyMap(toFetch, (resolved) =>
|
||||
let cancelled = false
|
||||
setStatusMap(prev => ({ ...prev, ...toLoadingMap(toFetch) }))
|
||||
fetchFreeBusyMap(toFetch, resolved =>
|
||||
Promise.all(
|
||||
resolved.map(async ({ email, userId }) => {
|
||||
try {
|
||||
const busy = await getFreeBusyForAddedAttendeesREPORT(
|
||||
userId,
|
||||
moment.tz(start, timezone).utc().format("YYYYMMDDTHHmmss"),
|
||||
moment.tz(end, timezone).utc().format("YYYYMMDDTHHmmss")
|
||||
);
|
||||
return [email, (busy ? "busy" : "free") as FreeBusyStatus] as const;
|
||||
moment.tz(start, timezone).utc().format('YYYYMMDDTHHmmss'),
|
||||
moment.tz(end, timezone).utc().format('YYYYMMDDTHHmmss')
|
||||
)
|
||||
return [email, (busy ? 'busy' : 'free') as FreeBusyStatus] as const
|
||||
} catch {
|
||||
return [email, "unknown" as FreeBusyStatus] as const;
|
||||
return [email, 'unknown' as FreeBusyStatus] as const
|
||||
}
|
||||
})
|
||||
).then(Object.fromEntries)
|
||||
)
|
||||
.then((updates) => {
|
||||
.then(updates => {
|
||||
if (!cancelled) {
|
||||
Object.keys(updates).forEach((e) => {
|
||||
fetchedNewEmailsRef.current.add(e);
|
||||
});
|
||||
setStatusMap((prev) => ({ ...prev, ...updates }));
|
||||
Object.keys(updates).forEach(e => {
|
||||
fetchedNewEmailsRef.current.add(e)
|
||||
})
|
||||
setStatusMap(prev => ({ ...prev, ...updates }))
|
||||
}
|
||||
return
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
toFetch.forEach((a) => {
|
||||
fetchedNewEmailsRef.current.add(a.email);
|
||||
});
|
||||
setStatusMap((prev) => ({ ...prev, ...toUnknownMap(toFetch) }));
|
||||
toFetch.forEach(a => {
|
||||
fetchedNewEmailsRef.current.add(a.email)
|
||||
})
|
||||
setStatusMap(prev => ({ ...prev, ...toUnknownMap(toFetch) }))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
cancelled = true
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [newKey, start, end, enabled, timezone]);
|
||||
}, [newKey, start, end, enabled, timezone])
|
||||
|
||||
return statusMap;
|
||||
return statusMap
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { isValidEmail } from "../../utils/isValidEmail";
|
||||
import type { User } from "./PeopleSearch";
|
||||
import { useCallback } from 'react'
|
||||
import type { SyntheticEvent } from 'react'
|
||||
import { isValidEmail } from '../../utils/isValidEmail'
|
||||
import type { User } from './PeopleSearch'
|
||||
|
||||
export function usePasteHandler({
|
||||
freeSolo,
|
||||
@@ -9,62 +9,62 @@ export function usePasteHandler({
|
||||
onChange,
|
||||
setQuery,
|
||||
setInputError,
|
||||
t,
|
||||
t
|
||||
}: {
|
||||
freeSolo?: boolean;
|
||||
selectedUsers: User[];
|
||||
onChange: (event: SyntheticEvent, users: User[]) => void;
|
||||
setQuery: (value: string) => void;
|
||||
setInputError: (error: string | null) => void;
|
||||
t: (key: string) => string;
|
||||
freeSolo?: boolean
|
||||
selectedUsers: User[]
|
||||
onChange: (event: SyntheticEvent, users: User[]) => void
|
||||
setQuery: (value: string) => void
|
||||
setInputError: (error: string | null) => void
|
||||
t: (key: string) => string
|
||||
}) {
|
||||
return useCallback(
|
||||
(event: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
if (!freeSolo) return;
|
||||
if (!freeSolo) return
|
||||
|
||||
const pasted = event.clipboardData.getData("text/plain");
|
||||
if (!pasted) return;
|
||||
const pasted = event.clipboardData.getData('text/plain')
|
||||
if (!pasted) return
|
||||
|
||||
// Split by comma, semicolon, newline, or whitespace
|
||||
const chunks = pasted
|
||||
.split(/[,;\n\r\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
// If there is only one chunk, let the default Autocomplete behaviour handle it
|
||||
if (chunks.length <= 1) return;
|
||||
if (chunks.length <= 1) return
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault()
|
||||
|
||||
const existingEmails = new Set(selectedUsers.map((u) => u.email));
|
||||
const validUsers: User[] = [];
|
||||
const invalid: string[] = [];
|
||||
const existingEmails = new Set(selectedUsers.map(u => u.email))
|
||||
const validUsers: User[] = []
|
||||
const invalid: string[] = []
|
||||
|
||||
for (const chunk of chunks) {
|
||||
if (!isValidEmail(chunk)) {
|
||||
invalid.push(chunk);
|
||||
invalid.push(chunk)
|
||||
} else if (!existingEmails.has(chunk)) {
|
||||
existingEmails.add(chunk);
|
||||
validUsers.push({ email: chunk, displayName: chunk });
|
||||
existingEmails.add(chunk)
|
||||
validUsers.push({ email: chunk, displayName: chunk })
|
||||
}
|
||||
// silently skip duplicates
|
||||
}
|
||||
|
||||
if (validUsers.length > 0) {
|
||||
onChange(event, [...selectedUsers, ...validUsers]);
|
||||
onChange(event, [...selectedUsers, ...validUsers])
|
||||
}
|
||||
|
||||
if (invalid.length > 0) {
|
||||
// Leave the invalid text in the input for manual correction
|
||||
setQuery(invalid.join(", "));
|
||||
setQuery(invalid.join(', '))
|
||||
setInputError(
|
||||
t("peopleSearch.invalidEmail").replace("%{email}", invalid.join(", "))
|
||||
);
|
||||
t('peopleSearch.invalidEmail').replace('%{email}', invalid.join(', '))
|
||||
)
|
||||
} else {
|
||||
setQuery("");
|
||||
setInputError(null);
|
||||
setQuery('')
|
||||
setInputError(null)
|
||||
}
|
||||
},
|
||||
[freeSolo, selectedUsers, onChange, setQuery, setInputError, t]
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { searchUsers } from "@/features/User/userAPI";
|
||||
import { useState, useEffect } from 'react'
|
||||
import { searchUsers } from '@/features/User/userAPI'
|
||||
|
||||
export interface UseUserSearchProps {
|
||||
objectTypes: string[];
|
||||
errorMessage: string;
|
||||
objectTypes: string[]
|
||||
errorMessage: string
|
||||
}
|
||||
|
||||
export function useUserSearch<T>({
|
||||
objectTypes,
|
||||
errorMessage,
|
||||
errorMessage
|
||||
}: UseUserSearchProps) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [options, setOptions] = useState<T[]>([]);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [query, setQuery] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [options, setOptions] = useState<T[]>([])
|
||||
const [hasSearched, setHasSearched] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const [inputError, setInputError] = useState<string | null>(null);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
const [inputError, setInputError] = useState<string | null>(null)
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let cancelled = false
|
||||
|
||||
const delayDebounceFn = setTimeout(async () => {
|
||||
if (!query.trim()) {
|
||||
if (!cancelled) {
|
||||
setOptions([]);
|
||||
setLoading(false);
|
||||
setHasSearched(false);
|
||||
setOptions([])
|
||||
setLoading(false)
|
||||
setHasSearched(false)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setLoading(true);
|
||||
setHasSearched(false);
|
||||
setLoading(true)
|
||||
setHasSearched(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await searchUsers(query, objectTypes);
|
||||
const res = await searchUsers(query, objectTypes)
|
||||
if (!cancelled) {
|
||||
setOptions(res as unknown as T[]);
|
||||
setHasSearched(true);
|
||||
setOptions(res as unknown as T[])
|
||||
setHasSearched(true)
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setHasSearched(false);
|
||||
setSnackbarMessage(errorMessage);
|
||||
setSnackbarOpen(true);
|
||||
setHasSearched(false)
|
||||
setSnackbarMessage(errorMessage)
|
||||
setSnackbarOpen(true)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}, 300)
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(delayDebounceFn);
|
||||
};
|
||||
}, [objectTypes, query, errorMessage]);
|
||||
cancelled = true
|
||||
clearTimeout(delayDebounceFn)
|
||||
}
|
||||
}, [objectTypes, query, errorMessage])
|
||||
|
||||
return {
|
||||
query,
|
||||
@@ -76,6 +76,6 @@ export function useUserSearch<T>({
|
||||
snackbarOpen,
|
||||
setSnackbarOpen,
|
||||
snackbarMessage,
|
||||
setSnackbarMessage,
|
||||
};
|
||||
setSnackbarMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
exportCalendar,
|
||||
getSecretLink,
|
||||
} from "@/features/Calendars/CalendarApi";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { exportCalendar, getSecretLink } from '@/features/Calendars/CalendarApi'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -10,90 +7,90 @@ import {
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@linagora/twake-mui";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
import { FieldWithLabel } from "../Event/components/FieldWithLabel";
|
||||
import { SnackbarAlert } from "../Loading/SnackBarAlert";
|
||||
import { CalendarAccessRights, UserWithAccess } from "./CalendarAccessRights";
|
||||
Typography
|
||||
} from '@linagora/twake-mui'
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { ErrorSnackbar } from '../Error/ErrorSnackbar'
|
||||
import { FieldWithLabel } from '../Event/components/FieldWithLabel'
|
||||
import { SnackbarAlert } from '../Loading/SnackBarAlert'
|
||||
import { CalendarAccessRights, UserWithAccess } from './CalendarAccessRights'
|
||||
|
||||
interface AccessTabProps {
|
||||
calendar: Calendar;
|
||||
usersWithAccess: UserWithAccess[];
|
||||
onUsersWithAccessChange: (users: UserWithAccess[]) => void;
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void;
|
||||
calendar: Calendar
|
||||
usersWithAccess: UserWithAccess[]
|
||||
onUsersWithAccessChange: (users: UserWithAccess[]) => void
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void
|
||||
}
|
||||
|
||||
export function AccessTab({
|
||||
calendar,
|
||||
usersWithAccess,
|
||||
onUsersWithAccessChange,
|
||||
onInvitesLoaded,
|
||||
onInvitesLoaded
|
||||
}: AccessTabProps) {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace(".json", "")}`;
|
||||
const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace('.json', '')}`
|
||||
|
||||
const isResource = useMemo(
|
||||
() => calendar?.owner?.resource,
|
||||
[calendar?.owner?.resource]
|
||||
);
|
||||
)
|
||||
|
||||
const [secretLink, setSecretLink] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [secretLink, setSecretLink] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSecret() {
|
||||
const existing = await getSecretLink(
|
||||
calendar.link.replace(".json", ""),
|
||||
calendar.link.replace('.json', ''),
|
||||
false
|
||||
);
|
||||
setSecretLink(existing.secretLink);
|
||||
)
|
||||
setSecretLink(existing.secretLink)
|
||||
}
|
||||
fetchSecret();
|
||||
}, [calendar.link]);
|
||||
fetchSecret()
|
||||
}, [calendar.link])
|
||||
|
||||
const handleCopy = (content: string) => {
|
||||
navigator.clipboard.writeText(content);
|
||||
setOpen(true);
|
||||
};
|
||||
navigator.clipboard.writeText(content)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleResetSecretLink = async () => {
|
||||
const newSecret = await getSecretLink(
|
||||
calendar.link.replace(".json", ""),
|
||||
calendar.link.replace('.json', ''),
|
||||
true
|
||||
);
|
||||
setSecretLink(newSecret.secretLink);
|
||||
};
|
||||
)
|
||||
setSecretLink(newSecret.secretLink)
|
||||
}
|
||||
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
const [exportError, setExportError] = useState("");
|
||||
const [exportLoading, setExportLoading] = useState(false)
|
||||
const [exportError, setExportError] = useState('')
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setExportLoading(true);
|
||||
setExportLoading(true)
|
||||
const exportedData = await exportCalendar(
|
||||
calendar.link.replace(".json", "")
|
||||
);
|
||||
const blob = new Blob([exportedData], { type: "text/calendar" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${calendar.id.split("/")[1]}.ics`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
calendar.link.replace('.json', '')
|
||||
)
|
||||
const blob = new Blob([exportedData], { type: 'text/calendar' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${calendar.id.split('/')[1]}.ics`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
setExportError((e as Error).message);
|
||||
setExportError((e as Error).message)
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
setExportLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -105,13 +102,13 @@ export function AccessTab({
|
||||
/>
|
||||
|
||||
{!!window.DAV_BASE_URL && !isResource && (
|
||||
<FieldWithLabel label={t("calendar.caldav_access")} isExpanded={false}>
|
||||
<FieldWithLabel label={t('calendar.caldav_access')} isExpanded={false}>
|
||||
<Box mt={2}>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
label=""
|
||||
inputProps={{ "aria-label": t("calendar.caldav_access") }}
|
||||
inputProps={{ 'aria-label': t('calendar.caldav_access') }}
|
||||
value={calDAVLink}
|
||||
size="small"
|
||||
InputProps={{
|
||||
@@ -124,20 +121,20 @@ export function AccessTab({
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
)}
|
||||
|
||||
<FieldWithLabel label={t("calendar.secretUrl")} isExpanded={false}>
|
||||
<FieldWithLabel label={t('calendar.secretUrl')} isExpanded={false}>
|
||||
<Box mt={3} display="flex" alignItems="center" gap={1}>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
label=""
|
||||
inputProps={{ "aria-label": t("calendar.secretUrl") }}
|
||||
inputProps={{ 'aria-label': t('calendar.secretUrl') }}
|
||||
value={secretLink}
|
||||
size="small"
|
||||
InputProps={{
|
||||
@@ -147,32 +144,32 @@ export function AccessTab({
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={handleResetSecretLink}
|
||||
sx={{ borderRadius: "4px" }}
|
||||
sx={{ borderRadius: '4px' }}
|
||||
>
|
||||
{t("actions.reset")}
|
||||
{t('actions.reset')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.secondary", mt: 1, lineHeight: 1.5 }}
|
||||
sx={{ color: 'text.secondary', mt: 1, lineHeight: 1.5 }}
|
||||
>
|
||||
{t("calendar.secretUrlDesc")}
|
||||
{t('calendar.secretUrlDesc')}
|
||||
</Typography>
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel label={t("calendar.exportCalendar")} isExpanded={false}>
|
||||
<FieldWithLabel label={t('calendar.exportCalendar')} isExpanded={false}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.secondary", my: 1, lineHeight: 1.5 }}
|
||||
sx={{ color: 'text.secondary', my: 1, lineHeight: 1.5 }}
|
||||
>
|
||||
{t("calendar.exportDesc")}
|
||||
{t('calendar.exportDesc')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -180,15 +177,15 @@ export function AccessTab({
|
||||
onClick={handleExport}
|
||||
startIcon={!exportLoading && <FileDownloadOutlinedIcon />}
|
||||
disabled={exportLoading}
|
||||
sx={{ borderRadius: "4px" }}
|
||||
sx={{ borderRadius: '4px' }}
|
||||
>
|
||||
{exportLoading ? (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<CircularProgress size={18} />
|
||||
{t("actions.exporting")}
|
||||
{t('actions.exporting')}
|
||||
</Box>
|
||||
) : (
|
||||
t("actions.export")
|
||||
t('actions.export')
|
||||
)}
|
||||
</Button>
|
||||
</FieldWithLabel>
|
||||
@@ -196,9 +193,9 @@ export function AccessTab({
|
||||
<SnackbarAlert
|
||||
setOpen={setOpen}
|
||||
open={open}
|
||||
message={t("common.link_copied")}
|
||||
message={t('common.link_copied')}
|
||||
/>
|
||||
<ErrorSnackbar error={exportError} type="calendar" />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user