Skip to content

Commit 40b2a61

Browse files
Fidesnoellarav3n11
authored andcommitted
chore(frontend): allow users to request invitation codes
- add a customLink component that is used across the entire app
1 parent 3fa12d2 commit 40b2a61

31 files changed

+1789
-632
lines changed

frontend-new/.storybook/main.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const config: StorybookConfig = {
1414
"FIREBASE_AUTH_DOMAIN": btoa("some-domain"),
1515
"BACKEND_URL": btoa("http://foo.bar.com/api"),
1616
"SENSITIVE_PERSONAL_DATA_RSA_ENCRYPTION_KEY": btoa(\`${key}\`),
17-
"SENSITIVE_PERSONAL_DATA_RSA_ENCRYPTION_KEY_ID": btoa("1")
17+
"SENSITIVE_PERSONAL_DATA_RSA_ENCRYPTION_KEY_ID": btoa("1"),
18+
"SENTRY_FRONTEND_DSN": btoa("https://[email protected]/baz")
1819
};
1920
//used for chat components
2021
sessionStorage.setItem("ChatSessionID", "1234")

frontend-new/.storybook/preview.tsx

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React, { useEffect } from "react";
12
import { CssBaseline, ThemeProvider } from "@mui/material";
23
import { HashRouter } from "react-router-dom";
34
import { applicationTheme, ThemeMode } from "../src/theme/applicationTheme/applicationTheme";
@@ -19,6 +20,7 @@ import "@fontsource/roboto/700.css";
1920
import type { Preview, StoryFn, StoryObj } from "@storybook/react";
2021
import SnackbarProvider from "../src/theme/SnackbarProvider/SnackbarProvider";
2122
import { IsOnlineContext } from "../src/app/isOnlineProvider/IsOnlineProvider";
23+
import { initSentry } from "../src/sentryInit";
2224

2325
const preview: Preview = {
2426
parameters: {
@@ -58,6 +60,17 @@ const preview: Preview = {
5860
dynamicTitle: true,
5961
},
6062
},
63+
sentryEnabled: {
64+
name: 'Sentry Enabled',
65+
description: 'Enable/disable Sentry error reporting',
66+
toolbar: {
67+
icon: 'alert',
68+
items: [
69+
{value: true, title: 'Enabled'},
70+
{value: false, title: 'Disabled'},
71+
],
72+
},
73+
},
6174
},
6275
args: {
6376
online: true,
@@ -66,9 +79,35 @@ const preview: Preview = {
6679

6780
export default preview;
6881

82+
// Store the original SENTRY_FRONTEND_DSN value
83+
// @ts-ignore
84+
const ORIGINAL_SENTRY_DSN = window.tabiyaConfig.SENTRY_FRONTEND_DSN;
85+
let isSentryInitialized = true;
86+
6987
export const decorators = [
70-
( Story: StoryFn, context: { globals: { online: any; }; }) => {
88+
( Story: StoryFn, context: { globals: { online: any; sentryEnabled: boolean; }; }) => {
7189
const isOnline = context.globals.online;
90+
const sentryEnabled = context.globals.sentryEnabled;
91+
const prevSentryEnabled = React.useRef(sentryEnabled);
92+
93+
useEffect(() => {
94+
if (prevSentryEnabled.current !== sentryEnabled) {
95+
if (sentryEnabled) {
96+
// @ts-ignore
97+
window.tabiyaConfig.SENTRY_FRONTEND_DSN = ORIGINAL_SENTRY_DSN;
98+
initSentry();
99+
isSentryInitialized = true;
100+
} else {
101+
// @ts-ignore
102+
window.tabiyaConfig.SENTRY_FRONTEND_DSN = undefined;
103+
isSentryInitialized = false;
104+
// we have to reload since there is no way to notify the components of this change
105+
// it is not a provider and the init happens outside of even the react root.render()
106+
window.location.reload();
107+
}
108+
prevSentryEnabled.current = sentryEnabled;
109+
}
110+
}, [sentryEnabled]);
72111

73112
return (
74113
<HashRouter>

frontend-new/src/auth/components/SocialAuth/SocialAuth.tsx

+19-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from "react";
22
import "firebaseui/dist/firebaseui.css";
33
import { useSnackbar } from "src/theme/SnackbarProvider/SnackbarProvider";
44
import { IsOnlineContext } from "src/app/isOnlineProvider/IsOnlineProvider";
5-
import { Box, Button, Typography } from "@mui/material";
5+
import { Box, Button, Divider, Typography, useTheme } from "@mui/material";
66
import FirebaseSocialAuthenticationService from "src/auth/services/FirebaseAuthenticationService/socialAuth/FirebaseSocialAuthentication.service";
77
import { FirebaseError, getUserFriendlyFirebaseErrorMessage } from "src/error/FirebaseError/firebaseError";
88
import { writeFirebaseErrorToLog } from "src/error/FirebaseError/logger";
@@ -47,6 +47,7 @@ const SocialAuth: React.FC<Readonly<SocialAuthProps>> = ({
4747
}) => {
4848
const isOnline = useContext(IsOnlineContext);
4949

50+
const theme = useTheme();
5051
const { enqueueSnackbar } = useSnackbar();
5152

5253
const [_registrationCode, setRegistrationCode] = useState(registrationCode);
@@ -159,13 +160,25 @@ const SocialAuth: React.FC<Readonly<SocialAuthProps>> = ({
159160
flexDirection="column"
160161
justifyContent="center"
161162
alignItems="center"
162-
mt={(theme) => theme.tabiyaSpacing.lg}
163+
width="100%"
163164
data-testid={DATA_TEST_ID.FIREBASE_AUTH_CONTAINER}
164165
>
165-
<Typography variant="caption" mt={2} data-testid={DATA_TEST_ID.CONTINUE_WITH_GOOGLE}>
166-
Or continue with
167-
</Typography>
168-
<Box mt={2} width="100%">
166+
<Divider
167+
textAlign="center"
168+
sx={{
169+
width: "100%",
170+
paddingY: theme.fixedSpacing(theme.tabiyaSpacing.xs),
171+
}}
172+
>
173+
<Typography
174+
variant="subtitle2"
175+
padding={theme.fixedSpacing(theme.tabiyaSpacing.sm)}
176+
data-testid={DATA_TEST_ID.CONTINUE_WITH_GOOGLE}
177+
>
178+
Or continue with
179+
</Typography>
180+
</Divider>
181+
<Box width="100%">
169182
<div data-test_id={DATA_TEST_ID.FIREBASE_AUTH}>
170183
<Button
171184
variant="text"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// silence chatty console
2+
import "src/_test_utilities/consoleMock";
3+
4+
import React from "react";
5+
import { render, screen, fireEvent, act } from "src/_test_utilities/test-utils";
6+
import userEvent from "@testing-library/user-event";
7+
import RegistrationCodeFormModal, {
8+
DATA_TEST_ID,
9+
RegistrationCodeFormModalState,
10+
} from "src/auth/components/registrationCodeFormModal/RegistrationCodeFormModal";
11+
import RequestInvitationCodeFormModal from "src/auth/components/requestInvitationCode/requestInvitationCodeFormModal/RequestInvitationCodeFormModal";
12+
import * as Sentry from "@sentry/react";
13+
14+
// Mock InvitationsService
15+
jest.mock("src/auth/services/invitationsService/invitations.service", () => ({
16+
getInstance: jest.fn().mockReturnValue({
17+
checkInvitationCodeStatus: jest.fn(),
18+
}),
19+
}));
20+
21+
// mock the snack bar provider
22+
jest.mock("src/theme/SnackbarProvider/SnackbarProvider", () => {
23+
const actual = jest.requireActual("src/theme/SnackbarProvider/SnackbarProvider");
24+
return {
25+
...actual,
26+
__esModule: true,
27+
useSnackbar: jest.fn().mockReturnValue({
28+
enqueueSnackbar: jest.fn(),
29+
closeSnackbar: jest.fn(),
30+
}),
31+
};
32+
});
33+
34+
// mock the RequestInvitationFormModal component
35+
jest.mock("src/auth/components/requestInvitationCode/requestInvitationCodeFormModal/RequestInvitationCodeFormModal", () => {
36+
const actual = jest.requireActual(
37+
"src/auth/components/requestInvitationCode/requestInvitationCodeFormModal/RequestInvitationCodeFormModal"
38+
);
39+
return {
40+
...actual,
41+
__esModule: true,
42+
default: jest.fn().mockImplementation(() => {
43+
return <span data-testid={actual.DATA_TEST_ID.CONTAINER}></span>;
44+
}),
45+
};
46+
});
47+
48+
// Mock Sentry
49+
jest.mock("@sentry/react");
50+
51+
describe("RegistrationCodeFormModal", () => {
52+
test("renders correctly when modal is shown and call onSuccess with the provided code", () => {
53+
// GIVEN the component is shown
54+
const givenShown = RegistrationCodeFormModalState.SHOW;
55+
// AND GIVEN Sentry is initialized
56+
jest.spyOn(Sentry, "isInitialized").mockReturnValue(true);
57+
58+
// AND the onClose function is a jest function
59+
const givenOnSuccess = jest.fn();
60+
61+
// WHEN the component is rendered
62+
render(<RegistrationCodeFormModal onClose={jest.fn} modalState={givenShown} onSuccess={givenOnSuccess} />);
63+
64+
// THEN the component should render correctly
65+
const container = screen.getByTestId(DATA_TEST_ID.CONTAINER);
66+
expect(container).toBeInTheDocument();
67+
68+
// AND the component should contain the necessary elements
69+
expect(screen.getByTestId(DATA_TEST_ID.MODAL_TITLE)).toBeInTheDocument();
70+
expect(screen.getByTestId(DATA_TEST_ID.MODAL_SUBTITLE)).toBeInTheDocument();
71+
expect(screen.getByTestId(DATA_TEST_ID.INVITATION_CODE_INPUT)).toBeInTheDocument();
72+
expect(screen.getByTestId(DATA_TEST_ID.SUBMIT_BUTTON)).toBeInTheDocument();
73+
expect(screen.getByTestId(DATA_TEST_ID.CLOSE_ICON)).toBeInTheDocument();
74+
expect(screen.getByTestId(DATA_TEST_ID.REQUEST_REGISTRATION_CODE_LINK)).toBeInTheDocument();
75+
76+
// AND the component should match the snapshot
77+
expect(container).toMatchSnapshot();
78+
79+
// WHEN the registration code is entered
80+
const registrationCode = "foo";
81+
fireEvent.change(screen.getByTestId(DATA_TEST_ID.INVITATION_CODE_INPUT), { target: { value: registrationCode } });
82+
83+
// AND the registration code is submitted
84+
fireEvent.click(screen.getByTestId(DATA_TEST_ID.SUBMIT_BUTTON));
85+
86+
// THEN the onSuccess function should be called with the registration code
87+
expect(givenOnSuccess).toHaveBeenCalledWith(registrationCode);
88+
});
89+
90+
test("should show request registration code link when Sentry is initialized", () => {
91+
// GIVEN Sentry is initialized
92+
jest.spyOn(Sentry, "isInitialized").mockReturnValue(true);
93+
// AND the component is shown
94+
const givenShown = RegistrationCodeFormModalState.SHOW;
95+
96+
// WHEN the component is rendered
97+
render(<RegistrationCodeFormModal onClose={jest.fn()} modalState={givenShown} onSuccess={jest.fn()} />);
98+
99+
// THEN the request registration code link should be visible
100+
expect(screen.getByTestId(DATA_TEST_ID.REQUEST_REGISTRATION_CODE_LINK)).toBeInTheDocument();
101+
});
102+
103+
test("should not show request registration code link when Sentry is not initialized", () => {
104+
// GIVEN Sentry is not initialized
105+
jest.spyOn(Sentry, "isInitialized").mockReturnValue(false);
106+
// AND the component is shown
107+
const givenShown = RegistrationCodeFormModalState.SHOW;
108+
109+
// WHEN the component is rendered
110+
render(<RegistrationCodeFormModal onClose={jest.fn()} modalState={givenShown} onSuccess={jest.fn()} />);
111+
112+
// THEN the request registration code link should not be visible
113+
expect(screen.queryByTestId(DATA_TEST_ID.REQUEST_REGISTRATION_CODE_LINK)).not.toBeInTheDocument();
114+
});
115+
116+
test("should render CircularProgress when modal state is loading", () => {
117+
// GIVEN the component is in loading state
118+
const givenShown = RegistrationCodeFormModalState.LOADING;
119+
120+
// AND the onClose function is a jest function
121+
const givenOnClose = jest.fn();
122+
123+
// WHEN the component is rendered
124+
render(<RegistrationCodeFormModal onClose={givenOnClose} modalState={givenShown} onSuccess={jest.fn()} />);
125+
126+
// THEN the circular progress should be in the document
127+
const circularProgress = screen.getByTestId(DATA_TEST_ID.PROGRESS_ELEMENT);
128+
expect(circularProgress).toBeInTheDocument();
129+
});
130+
131+
test("closes the modal when the close icon is clicked", () => {
132+
// GIVEN the component is shown
133+
const givenShown = RegistrationCodeFormModalState.SHOW;
134+
135+
// AND the onClose function is a jest function
136+
const givenOnClose = jest.fn();
137+
138+
// WHEN the component is rendered
139+
render(<RegistrationCodeFormModal onClose={givenOnClose} modalState={givenShown} onSuccess={jest.fn()} />);
140+
141+
// THEN the component should render correctly
142+
const container = screen.getByTestId(DATA_TEST_ID.CONTAINER);
143+
expect(container).toBeInTheDocument();
144+
145+
// AND the component should contain the necessary elements
146+
expect(screen.getByTestId(DATA_TEST_ID.MODAL_TITLE)).toBeInTheDocument();
147+
expect(screen.getByTestId(DATA_TEST_ID.MODAL_SUBTITLE)).toBeInTheDocument();
148+
expect(screen.getByTestId(DATA_TEST_ID.INVITATION_CODE_INPUT)).toBeInTheDocument();
149+
expect(screen.getByTestId(DATA_TEST_ID.SUBMIT_BUTTON)).toBeInTheDocument();
150+
expect(screen.getByTestId(DATA_TEST_ID.CLOSE_ICON)).toBeInTheDocument();
151+
152+
// WHEN the close icon is clicked
153+
fireEvent.click(screen.getByTestId(DATA_TEST_ID.CLOSE_ICON));
154+
155+
// THEN the onClose function should be called
156+
expect(givenOnClose).toHaveBeenCalled();
157+
});
158+
159+
test("should show request invitation code modal when link is clicked and close it when onClose is called", async () => {
160+
// GIVEN the component is shown
161+
const givenShown = RegistrationCodeFormModalState.SHOW;
162+
const givenOnClose = jest.fn();
163+
// AND GIVEN Sentry is initialized
164+
jest.spyOn(Sentry, "isInitialized").mockReturnValue(true);
165+
// AND the component is rendered
166+
render(<RegistrationCodeFormModal modalState={givenShown} onSuccess={jest.fn()} onClose={givenOnClose} />);
167+
// AND onClose mock
168+
const onCloseMock = (RequestInvitationCodeFormModal as jest.Mock).mock.calls[0][0].onClose;
169+
170+
// WHEN the request invitation code link is clicked
171+
const requestRegistrationCodeLink = screen.getByTestId(DATA_TEST_ID.REQUEST_REGISTRATION_CODE_LINK);
172+
await userEvent.click(requestRegistrationCodeLink);
173+
174+
// THEN expect the invitation code form modal to be closed
175+
expect(givenOnClose).toHaveBeenCalled();
176+
// AND the request registration form modal should be displayed
177+
expect(RequestInvitationCodeFormModal).toHaveBeenCalledWith(
178+
expect.objectContaining({ open: true }),
179+
expect.anything()
180+
);
181+
182+
// WHEN the onClose function is called
183+
act(() => {
184+
onCloseMock();
185+
});
186+
187+
// THEN the request invitation code form modal should be closed
188+
expect(RequestInvitationCodeFormModal).toHaveBeenCalledWith(
189+
expect.objectContaining({ open: false }),
190+
expect.anything()
191+
);
192+
});
193+
});

0 commit comments

Comments
 (0)