Skip to content

Commit

Permalink
feat(ui): create notification option modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Vu Van Duc authored and Vu Van Duc committed Jul 15, 2024
1 parent 6f75d91 commit 3206756
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 4 deletions.
27 changes: 23 additions & 4 deletions src/locales/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -930,12 +930,31 @@
},
"sections": {
"new": "New",
"earlier": "Earlier"
"earlier": {
"title": "Earlier",
"end": "End of notifications",
"buttons": {
"showealier": "View previous notifications"
}
}
},
"optionmodal": {
"title": "Notification options",
"done": "Done",
"showdetail": "Show details",
"markasread": "Mark as read",
"markasunread": "Mark as unread",
"delete": "Delete notification",
"deletealert": {
"text": "Deleting this notification cannot be undone. Once deleted you will no longer be able to require any actions required from this notification.",
"accept": "Delete",
"cancel": "Cancel"
}
},
"labels": {
"exnipexgrant": "{{connection}} wants to issue you a credential",
"multisigicp": "{{connection}} is requesting to create a multi-sig identifier with you",
"exnipexapply": "{{connection}} has requested a credential from you"
"exnipexgrant": "<strong>{{connection}}</strong> wants to issue you a credential",
"multisigicp": "<strong>{{connection}}</strong> is requesting to create a multi-sig identifier with you",
"exnipexapply": "<strong>{{connection}}</strong> has requested a credential from you"
}
},
"details": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { waitForIonicReact } from "@ionic/react-test-utils";
import { AnyAction, Store } from "@reduxjs/toolkit";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { ReactNode } from "react";
import { act } from "react-dom/test-utils";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import EN_TRANSLATIONS from "../../../../locales/en/en.json";
import { TabsRoutePath } from "../../../../routes/paths";
import {
deleteNotification,
setReadedNotification,
} from "../../../../store/reducers/notificationsCache";
import { NotificationOptionsModal } from "./NotificationOptionsModal";

jest.mock("@ionic/react", () => ({
...jest.requireActual("@ionic/react"),
IonModal: ({ children }: { children: ReactNode }) => children,
}));

const notification = {
id: "AL3XmFY8BM9F604qmV-l9b0YMZNvshHG7X6CveMWKMmG",
createdAt: "2024-06-25T12:38:36.988Z",
a: {
r: "/exn/ipex/grant",
d: "EMT02ZHUhpnr4gFFk104B-pLwb2bJC8aip2VYmbPztnk",
m: "",
},
connectionId: "EMrT7qX0FIMenQoe5pJLahxz_rheks1uIviGW8ch8pfB",
read: false,
};

const deleteNotificationMock = jest.fn((id: string) => Promise.resolve(id));
const readNotificationMock = jest.fn((id: string) => Promise.resolve(id));
const unreadNotificationMock = jest.fn((id: string) => Promise.resolve(id));

jest.mock("../../../../core/agent/agent", () => ({
Agent: {
agent: {
signifyNotifications: {
deleteNotificationRecordById: (id: string) =>
deleteNotificationMock(id),
readNotification: (id: string) => readNotificationMock(id),
unreadNotification: (id: string) => unreadNotificationMock(id),
},
},
},
}));

describe("Notification Options modal", () => {
const dispatchMock = jest.fn();
let mockedStore: Store<unknown, AnyAction>;
beforeEach(() => {
jest.resetAllMocks();
const mockStore = configureStore();
const initialState = {
stateCache: {
routes: [TabsRoutePath.IDENTIFIERS],
authentication: {
loggedIn: true,
time: Date.now(),
passcodeIsSet: true,
passwordIsSet: true,
},
},
};
mockedStore = {
...mockStore(initialState),
dispatch: dispatchMock,
};
});

test("render", async () => {
const { getByTestId, getByText } = render(
<Provider store={mockedStore}>
<NotificationOptionsModal
optionsIsOpen
setOptionsIsOpen={jest.fn}
onShowDetail={jest.fn}
notification={notification}
/>
</Provider>
);

await waitForIonicReact();

expect(getByTestId("show-notification-detail")).toBeVisible();
expect(getByTestId("toogle-read-notification")).toBeVisible();
expect(getByTestId("delete-notification")).toBeVisible();
expect(
getByText(EN_TRANSLATIONS.notifications.tab.optionmodal.title)
).toBeVisible();
expect(
getByText(EN_TRANSLATIONS.notifications.tab.optionmodal.markasread)
).toBeVisible();
});

test("delete notification", async () => {
const { getByTestId, getByText } = render(
<Provider store={mockedStore}>
<NotificationOptionsModal
optionsIsOpen
setOptionsIsOpen={jest.fn}
onShowDetail={jest.fn}
notification={notification}
/>
</Provider>
);

await waitForIonicReact();

expect(getByTestId("delete-notification")).toBeVisible();

act(() => {
fireEvent.click(getByTestId("delete-notification"));
});

await waitFor(() => {
expect(
getByText(
EN_TRANSLATIONS.notifications.tab.optionmodal.deletealert.text
)
).toBeVisible();
});

act(() => {
fireEvent.click(getByTestId("alert-delete-notification-confirm-button"));
});

await waitFor(() => {
expect(deleteNotificationMock).toBeCalledWith(notification.id);
expect(dispatchMock).toBeCalledWith(deleteNotification(notification));
});
});

test("mask as read notification", async () => {
const { getByTestId } = render(
<Provider store={mockedStore}>
<NotificationOptionsModal
optionsIsOpen
setOptionsIsOpen={jest.fn}
onShowDetail={jest.fn}
notification={notification}
/>
</Provider>
);

await waitForIonicReact();

expect(getByTestId("toogle-read-notification")).toBeVisible();

act(() => {
fireEvent.click(getByTestId("toogle-read-notification"));
});

await waitFor(() => {
expect(readNotificationMock).toBeCalledWith(notification.id);
expect(dispatchMock).toBeCalledWith(
setReadedNotification({
id: notification.id,
read: !notification.read,
})
);
});
});

test("mask as unread notification", async () => {
const { getByTestId } = render(
<Provider store={mockedStore}>
<NotificationOptionsModal
optionsIsOpen
setOptionsIsOpen={jest.fn}
onShowDetail={jest.fn}
notification={{
...notification,
read: true,
}}
/>
</Provider>
);

await waitForIonicReact();

expect(getByTestId("toogle-read-notification")).toBeVisible();

act(() => {
fireEvent.click(getByTestId("toogle-read-notification"));
});

await waitFor(() => {
expect(unreadNotificationMock).toBeCalledWith(notification.id);
expect(dispatchMock).toBeCalledWith(
setReadedNotification({
id: notification.id,
read: false,
})
);
});
});

test("show notification detail", async () => {
const showNotiMock = jest.fn();
const { getByTestId } = render(
<Provider store={mockedStore}>
<NotificationOptionsModal
optionsIsOpen
setOptionsIsOpen={jest.fn}
onShowDetail={showNotiMock}
notification={notification}
/>
</Provider>
);

await waitForIonicReact();

expect(getByTestId("show-notification-detail")).toBeVisible();

act(() => {
fireEvent.click(getByTestId("show-notification-detail"));
});

await waitFor(() => {
expect(showNotiMock).toBeCalledWith(notification);
});
});
});
133 changes: 133 additions & 0 deletions src/ui/pages/Notifications/components/NotificationOptionsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
mailOpenOutline,
mailUnreadOutline,
readerOutline,
trashOutline,
} from "ionicons/icons";
import { useState } from "react";
import { Agent } from "../../../../core/agent/agent";
import { i18n } from "../../../../i18n";
import { useAppDispatch } from "../../../../store/hooks";
import {
deleteNotification,
setReadedNotification,
} from "../../../../store/reducers/notificationsCache";
import { Alert } from "../../../components/Alert";
import { OptionItem, OptionModal } from "../../../components/OptionsModal";
import { NotificationOptionModalProps } from "./NotificationOptionsModal.types";

const NotificationOptionsModal = ({
optionsIsOpen,
setOptionsIsOpen,
notification,
onShowDetail,
}: NotificationOptionModalProps) => {
const dispatch = useAppDispatch();
const [openAlert, setOpenAlert] = useState(false);

const closeModal = () => {
setOptionsIsOpen(false);
};

const toggleReadNotification = async () => {
try {
if (notification.read) {
await Agent.agent.signifyNotifications.unreadNotification(
notification.id
);
} else {
await Agent.agent.signifyNotifications.readNotification(
notification.id
);
}

dispatch(
setReadedNotification({
id: notification.id,
read: !notification.read,
})
);
closeModal();
} catch (e) {
// TODO: Handle error
}
};

const removeNotification = async () => {
try {
await Agent.agent.signifyNotifications.deleteNotificationRecordById(
notification.id
);
dispatch(deleteNotification(notification));
closeModal();
} catch (e) {
// TODO: Handle error
}
};

const deleteNotificationClick = () => {
setOpenAlert(true);
};

const options: OptionItem[] = [
{
icon: readerOutline,
label: i18n.t("notifications.tab.optionmodal.showdetail"),
onClick: () => {
onShowDetail(notification);
closeModal();
},
testId: "show-notification-detail",
},
{
icon: notification.read ? mailUnreadOutline : mailOpenOutline,
label: i18n.t(
notification.read
? "notifications.tab.optionmodal.markasunread"
: "notifications.tab.optionmodal.markasread"
),
onClick: toggleReadNotification,
testId: "toogle-read-notification",
},
{
icon: trashOutline,
label: i18n.t("notifications.tab.optionmodal.delete"),
onClick: deleteNotificationClick,
testId: "delete-notification",
},
];

return (
<>
<OptionModal
modalIsOpen={optionsIsOpen}
componentId="notification-options-modal"
onDismiss={closeModal}
header={{
closeButton: true,
closeButtonLabel: `${i18n.t("notifications.tab.optionmodal.done")}`,
closeButtonAction: closeModal,
title: `${i18n.t("notifications.tab.optionmodal.title")}`,
}}
items={options}
/>
<Alert
isOpen={openAlert}
setIsOpen={setOpenAlert}
dataTestId="alert-delete-notification"
headerText={i18n.t("notifications.tab.optionmodal.deletealert.text")}
confirmButtonText={`${i18n.t(
"notifications.tab.optionmodal.deletealert.accept"
)}`}
cancelButtonText={`${i18n.t(
"notifications.tab.optionmodal.deletealert.cancel"
)}`}
actionCancel={() => setOpenAlert(false)}
actionConfirm={removeNotification}
actionDismiss={() => setOpenAlert(false)}
/>
</>
);
};

export { NotificationOptionsModal };
Loading

0 comments on commit 3206756

Please sign in to comment.