Co-authored-by: Lê Nhân Phụng <lenhanphung@Phung-Mac-M4.local>
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
import { screen, fireEvent } from "@testing-library/react";
|
||||||
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
import ImportAlert from "../../../src/features/Events/ImportAlert";
|
||||||
|
import { clearError } from "../../../src/features/Calendars/CalendarSlice";
|
||||||
|
|
||||||
|
// 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", () => {
|
||||||
|
const preloadedState = {
|
||||||
|
calendars: {
|
||||||
|
list: {
|
||||||
|
cal1: {
|
||||||
|
id: "cal1",
|
||||||
|
name: "Calendar 1",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
title: "Event 1",
|
||||||
|
error: "Error message 1",
|
||||||
|
},
|
||||||
|
event2: {
|
||||||
|
uid: "event2",
|
||||||
|
title: "Event 2",
|
||||||
|
// No error
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cal2: {
|
||||||
|
id: "cal2",
|
||||||
|
name: "Calendar 2",
|
||||||
|
events: {
|
||||||
|
event3: {
|
||||||
|
uid: "event3",
|
||||||
|
title: "Event 3",
|
||||||
|
error: "Error message 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders nothing when there are no errors", () => {
|
||||||
|
const state = {
|
||||||
|
calendars: {
|
||||||
|
list: {
|
||||||
|
cal1: {
|
||||||
|
id: "cal1",
|
||||||
|
events: {
|
||||||
|
event1: { uid: "event1" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<ImportAlert />, state);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
// Let's adjust state for single error test.
|
||||||
|
const singleErrorState = {
|
||||||
|
calendars: {
|
||||||
|
list: {
|
||||||
|
cal1: {
|
||||||
|
id: "cal1",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
error: "Single Error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithProviders(<ImportAlert />, singleErrorState);
|
||||||
|
expect(screen.getByText("Single Error")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
// We should check what the real t function does or if it's mocked.
|
||||||
|
// Based on other tests, it seems we might need to check for the translation key or value.
|
||||||
|
// Let's assume standard behavior: "error.multipleEvents"
|
||||||
|
|
||||||
|
// Actually, EventErrorSnackbar uses: t("error.multipleEvents", { count: messages.length })
|
||||||
|
// If we don't have full i18n setup in tests, it might be tricky.
|
||||||
|
// But renderWithProviders likely sets up i18n.
|
||||||
|
|
||||||
|
// Let's check for the text that appears.
|
||||||
|
// If translation is missing, it might show "error.multipleEvents".
|
||||||
|
// Or if we can match regex.
|
||||||
|
|
||||||
|
// Let's try to match partial text or use a custom state with known behavior.
|
||||||
|
// If we look at EventErrorSnackbar:
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses error when close button is clicked", () => {
|
||||||
|
const singleErrorState = {
|
||||||
|
calendars: {
|
||||||
|
list: {
|
||||||
|
cal1: {
|
||||||
|
id: "cal1",
|
||||||
|
events: {
|
||||||
|
event1: {
|
||||||
|
uid: "event1",
|
||||||
|
error: "Dismiss Me",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,48 +1,56 @@
|
|||||||
import { Alert, Collapse } from "@mui/material";
|
import { useState, useMemo, useEffect } from "react";
|
||||||
import { useState } from "react";
|
|
||||||
import { useAppSelector } from "../../app/hooks";
|
import { useAppSelector } from "../../app/hooks";
|
||||||
|
import { EventErrorSnackbar } from "../../components/Error/ErrorSnackbar";
|
||||||
|
|
||||||
export default function ImportAlert() {
|
export default function ImportAlert() {
|
||||||
const [visibleAlerts, setVisibleAlerts] = useState<Record<string, boolean>>(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
const [dismissedErrors, setDismissedErrors] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
|
||||||
const toggleEventAlert = (eventId: string) => {
|
// Collect all errors from all events in all calendars
|
||||||
setVisibleAlerts((prev) => ({
|
const errors = useMemo(() => {
|
||||||
...prev,
|
const errorList: { id: string; message: string }[] = [];
|
||||||
[eventId]: false,
|
Object.values(calendars || {}).forEach((calendar) => {
|
||||||
}));
|
if (calendar.events) {
|
||||||
|
Object.values(calendar.events).forEach((event) => {
|
||||||
|
if (event.error) {
|
||||||
|
errorList.push({
|
||||||
|
id: event.uid,
|
||||||
|
message: event.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return errorList;
|
||||||
|
}, [calendars]);
|
||||||
|
|
||||||
|
// Filter out dismissed errors
|
||||||
|
const activeErrors = useMemo(() => {
|
||||||
|
return errors.filter((e) => !dismissedErrors.has(e.id));
|
||||||
|
}, [errors, dismissedErrors]);
|
||||||
|
|
||||||
|
const messages = activeErrors.map((e) => e.message);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// Mark all currently active errors as dismissed
|
||||||
|
setDismissedErrors((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
activeErrors.forEach((e) => next.add(e.id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// If new errors appear that were previously dismissed (e.g. re-fetch),
|
||||||
<>
|
// we might want to un-dismiss them, but for now simple dismissal is enough.
|
||||||
{Object.keys(calendars || {}).map((calendarId) =>
|
// Actually, if the error persists in the store, it's the same error.
|
||||||
calendars?.[calendarId]?.events
|
// If the user fixes it and it comes back, it might be a new fetch.
|
||||||
? Object.keys(calendars[calendarId]?.events)
|
// But usually uid is stable.
|
||||||
.filter((id) => calendars[calendarId]?.events[id].error)
|
|
||||||
.map((id) => {
|
|
||||||
const isVisible =
|
|
||||||
visibleAlerts[calendars[calendarId].events[id].uid] ?? true; // default to visible
|
|
||||||
|
|
||||||
return (
|
if (messages.length === 0) {
|
||||||
<Collapse
|
return null;
|
||||||
in={isVisible}
|
}
|
||||||
key={calendars[calendarId].events[id].uid}
|
|
||||||
>
|
return <EventErrorSnackbar messages={messages} onClose={handleClose} />;
|
||||||
<Alert
|
|
||||||
severity="error"
|
|
||||||
onClose={() =>
|
|
||||||
toggleEventAlert(calendars[calendarId].events[id].uid)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{calendars[calendarId].events[id].error}
|
|
||||||
</Alert>
|
|
||||||
</Collapse>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: []
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user