From fd588b474990d9ac231c30e11e0307bfb1faa34a Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:15:47 +0100 Subject: [PATCH] [#326] fixed attendees preview string + extensive tests (#357) Co-authored-by: Camille Moussu --- .../features/Events/EventDisplay.test.tsx | 610 ++++++++++++++++++ src/features/Events/EventDisplayPreview.tsx | 44 +- src/locales/en.json | 2 + src/locales/fr.json | 4 +- src/locales/ru.json | 2 + src/locales/vi.json | 2 + 6 files changed, 651 insertions(+), 13 deletions(-) diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index 5127fec..742f94b 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -819,6 +819,616 @@ describe("Event Preview Display", () => { expect(screen.getByText("Calendar")).toBeInTheDocument(); }); + + describe("Attendee Preview Display", () => { + const mockOnClose = jest.fn(); + const day = new Date("2025-01-15T10:00:00.000Z"); + + beforeEach(() => { + jest.clearAllMocks(); + (window as any).MAIL_SPA_URL = null; + }); + + const createStateWithAttendees = (attendees: any[]) => ({ + user: { + userData: { + sub: "test", + email: "test@test.com", + sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro", + openpaasId: "667037022b752d0026472254", + }, + organiserData: { + cn: "test", + cal_address: "test@test.com", + }, + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + id: "667037022b752d0026472254/cal1", + name: "Calendar", + color: "#FF0000", + events: { + event1: { + uid: "event1", + title: "Test Event", + calId: "667037022b752d0026472254/cal1", + start: day.toISOString(), + end: day.toISOString(), + organizer: { + cn: "organizer", + cal_address: "organizer@test.com", + }, + attendee: attendees, + }, + }, + ownerEmails: ["test@test.com"], + }, + }, + pending: false, + }, + }); + + describe("Guest count display", () => { + it("displays correct guest count including organizer", () => { + const attendees = [ + { + cn: "organizer", + cal_address: "organizer@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 2", + cal_address: "guest2@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + // Should display "3 guests" (organizer + 2 guests) + expect(screen.getByText(/3/)).toBeInTheDocument(); + }); + }); + + describe("All attendees with single status", () => { + it("displays only yes count when all attendees accepted", () => { + const attendees = [ + { + cn: "organizer", + cal_address: "organizer@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 2", + cal_address: "guest2@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText("eventPreview.yesCount(count=3)") + ).toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.maybeCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.needActionCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.noCount\b/i) + ).not.toBeInTheDocument(); + }); + + it("displays only maybe count when all attendees tentative", () => { + const attendees = [ + { + cn: "organizer", + cal_address: "organizer@test.com", + partstat: "TENTATIVE", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "TENTATIVE", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText("eventPreview.maybeCount(count=2)") + ).toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.yesCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.needActionCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.noCount\b/i) + ).not.toBeInTheDocument(); + }); + + it("displays only no count when all attendees declined", () => { + const attendees = [ + { + cn: "organizer", + cal_address: "organizer@test.com", + partstat: "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 2", + cal_address: "guest2@test.com", + partstat: "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 3", + cal_address: "guest3@test.com", + partstat: "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText("eventPreview.noCount(count=4)") + ).toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.yesCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.maybeCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.needActionCount/i) + ).not.toBeInTheDocument(); + }); + + it("displays only waiting count when all attendees need action", () => { + const attendees = [ + { + cn: "organizer", + cal_address: "organizer@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText("eventPreview.needActionCount(count=2)") + ).toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.yesCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.maybeCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.noCount/i) + ).not.toBeInTheDocument(); + }); + }); + + describe("No display when count is 0", () => { + it("does not display attendee preview when no attendees", () => { + const state = createStateWithAttendees([]); + renderWithProviders( + , + state + ); + + expect( + screen.queryByTestId("PeopleAltOutlinedIcon") + ).not.toBeInTheDocument(); + expect(screen.queryByText(/guests/i)).not.toBeInTheDocument(); + }); + }); + + describe("Regression tests: mixed statuses with correct counts", () => { + it("displays all statuses when attendees have mixed responses", () => { + const attendees = [ + { + cn: "organizer", + cal_address: "organizer@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 2", + cal_address: "guest2@test.com", + partstat: "TENTATIVE", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 3", + cal_address: "guest3@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 4", + cal_address: "guest4@test.com", + partstat: "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText(/eventPreview.yesCount\(count\=2\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.maybeCount\(count\=1\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.needActionCount\(count\=1\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.noCount\(count\=1\)/i) + ).toBeInTheDocument(); + }); + + it("displays correct counts with multiple yes and maybe", () => { + const attendees = [ + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 2", + cal_address: "guest2@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 3", + cal_address: "guest3@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 4", + cal_address: "guest4@test.com", + partstat: "TENTATIVE", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 5", + cal_address: "guest5@test.com", + partstat: "TENTATIVE", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText(/eventPreview\.yesCount\(count\=3\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview\.maybeCount\(count\=2\)/i) + ).toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.noCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.needActionCount/i) + ).not.toBeInTheDocument(); + }); + + it("displays correct counts with waiting and declined", () => { + const attendees = [ + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 2", + cal_address: "guest2@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 3", + cal_address: "guest3@test.com", + partstat: "NEEDS-ACTION", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 4", + cal_address: "guest4@test.com", + partstat: "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText(/eventPreview.needActionCount\(count\=3\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.noCount\(count\=1\)/i) + ).toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.yesCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.maybeCount/i) + ).not.toBeInTheDocument(); + }); + + it("does not display status categories with zero count", () => { + const attendees = [ + { + cn: "Guest 1", + cal_address: "guest1@test.com", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + { + cn: "Guest 2", + cal_address: "guest2@test.com", + partstat: "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + }, + ]; + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText(/eventPreview.yesCount\(count\=1\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.noCount\(count\=1\)/i) + ).toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.maybeCount/i) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/eventPreview.needActionCount/i) + ).not.toBeInTheDocument(); + }); + + it("handles large number of attendees correctly", () => { + const attendees = Array.from({ length: 15 }, (_, i) => ({ + cn: `Guest ${i}`, + cal_address: `guest${i}@test.com`, + partstat: + i < 8 + ? "ACCEPTED" + : i < 11 + ? "TENTATIVE" + : i < 13 + ? "NEEDS-ACTION" + : "DECLINED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + })); + + const state = createStateWithAttendees(attendees); + renderWithProviders( + , + state + ); + + expect( + screen.getByText(/eventPreview.yesCount\(count\=8\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.maybeCount\(count\=3\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.needActionCount\(count\=2\)/i) + ).toBeInTheDocument(); + expect( + screen.getByText(/eventPreview.noCount\(count\=2\)/i) + ).toBeInTheDocument(); + }); + }); + }); + describe("BUGFIX", () => { it("doesnt render anything next to date of all day preview", () => { const allDayState = { diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index 07d5a79..4c1a4f9 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -48,6 +48,8 @@ import { dlEvent } from "./EventApi"; import { CalendarEvent } from "./EventsTypes"; import EventUpdateModal from "./EventUpdateModal"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n"; +import { userAttendee } from "../User/userDataTypes"; + export default function EventPreviewModal({ eventId, calId, @@ -89,6 +91,7 @@ export default function EventPreviewModal({ undefined ); const [afterChoiceFunc, setAfterChoiceFunc] = useState(); + const attendeePreview = makeAttendeePreview(event.attendee, t); const [toggleActionMenu, setToggleActionMenu] = useState( null @@ -494,23 +497,13 @@ export default function EventPreviewModal({ {t("eventPreview.guests", { - count: attendees.length, + count: event.attendee.length, })} - {t("eventPreview.yesCount", { - count: attendees.filter( - (a) => a.partstat === "ACCEPTED" - ).length, - })} - ,{" "} - {t("eventPreview.noCount", { - count: attendees.filter( - (a) => a.partstat === "DECLINED" - ).length, - })} + {attendeePreview} {!showAllAttendees && ( @@ -843,3 +836,30 @@ function formatEnd( }); } } + +export function makeAttendeePreview(attendees: userAttendee[], t: Function) { + const attendeePreview = []; + const yesCount = attendees?.filter((a) => a.partstat === "ACCEPTED").length; + const noCount = attendees?.filter((a) => a.partstat === "DECLINED").length; + const maybeCount = attendees?.filter( + (a) => a.partstat === "TENTATIVE" + ).length; + const needActionCount = attendees?.filter( + (a) => a.partstat === "NEEDS-ACTION" + ).length; + if (yesCount) { + attendeePreview.push(t("eventPreview.yesCount", { count: yesCount })); + } + if (maybeCount) { + attendeePreview.push(t("eventPreview.maybeCount", { count: maybeCount })); + } + if (needActionCount) { + attendeePreview.push( + t("eventPreview.needActionCount", { count: needActionCount }) + ); + } + if (noCount) { + attendeePreview.push(t("eventPreview.noCount", { count: noCount })); + } + return attendeePreview.join(", "); +} diff --git a/src/locales/en.json b/src/locales/en.json index 16cd30f..2c35f38 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -207,6 +207,8 @@ "joinVideo": "Join the video conference", "guests": "%{count} guests", "yesCount": "%{count} yes", + "maybeCount": "%{count} maybe", + "needActionCount": "%{count} waiting", "noCount": "%{count} no", "recurrentEvent": "Recurrent Event", "freq": { diff --git a/src/locales/fr.json b/src/locales/fr.json index 69b7f10..ae3dba9 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -205,8 +205,10 @@ "showMore": "Afficher plus", "showLess": "Afficher moins", "joinVideo": "Rejoindre la visioconférence", - "guests": "%{count} invités", + "guests": "%{count} participants", "yesCount": "%{count} oui", + "maybeCount": "%{count} peut-être", + "needActionCount": "%{count} en attente", "noCount": "%{count} non", "recurrentEvent": "Événement récurrent", "freq": { diff --git a/src/locales/ru.json b/src/locales/ru.json index 784eaaf..6b5e016 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -207,6 +207,8 @@ "joinVideo": "Присоединиться к видеоконференции", "guests": "%{count} гостей", "yesCount": "%{count} да", + "maybeCount": "%{count} может быть", + "needActionCount": "%{count} в ожидании", "noCount": "%{count} нет", "recurrentEvent": "Повторяющееся событие", "freq": { diff --git a/src/locales/vi.json b/src/locales/vi.json index 4133ac6..8e14336 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -207,6 +207,8 @@ "joinVideo": "Tham gia cuộc họp video", "guests": "%{count} khách", "yesCount": "%{count} đồng ý", + "maybeCount": "%{count} có thể", + "needActionCount": "%{count} đang chờ", "noCount": "%{count} từ chối", "recurrentEvent": "Sự kiện lặp lại", "freq": {