@databiosphere/findable-ui 21.3.0 → 21.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/lib/components/Export/components/ExportForm/components/ExportButton/exportButton.js +6 -1
  4. package/lib/components/Export/components/ManifestDownload/components/ManifestDownloadEntity/components/FileManifestDownload/fileManifestDownload.js +5 -2
  5. package/lib/components/Index/components/AzulFileDownload/azulFileDownload.js +10 -5
  6. package/lib/components/Layout/components/Header/components/Content/components/Navigation/components/NavigationMenu/navigationMenu.js +1 -1
  7. package/lib/components/Layout/components/Header/components/Content/components/Navigation/components/NavigationMenuItems/navigationMenuItems.js +20 -21
  8. package/lib/components/Layout/components/Header/components/Content/components/Navigation/constants.d.ts +1 -0
  9. package/lib/components/Layout/components/Header/components/Content/components/Navigation/constants.js +1 -0
  10. package/lib/components/Layout/components/Header/components/Content/components/Navigation/navigation.d.ts +2 -1
  11. package/lib/components/Layout/components/Header/components/Content/components/Navigation/navigation.js +16 -17
  12. package/lib/components/Layout/components/Header/header.js +2 -1
  13. package/lib/components/Login/components/Button/types.d.ts +1 -1
  14. package/lib/components/Login/components/Buttons/buttons.d.ts +2 -0
  15. package/lib/components/Login/components/Buttons/buttons.js +5 -0
  16. package/lib/components/Login/components/Buttons/types.d.ts +8 -0
  17. package/lib/components/Login/components/Buttons/types.js +1 -0
  18. package/lib/components/Login/components/Section/components/Consent/consent.d.ts +3 -0
  19. package/lib/components/Login/components/Section/components/Consent/consent.js +14 -0
  20. package/lib/components/Login/components/Section/components/Consent/consent.styles.d.ts +7 -0
  21. package/lib/components/Login/components/Section/components/Consent/consent.styles.js +14 -0
  22. package/lib/components/Login/components/Section/components/Consent/types.d.ts +6 -0
  23. package/lib/components/Login/components/Section/components/Consent/types.js +1 -0
  24. package/lib/components/Login/components/Section/components/Warning/warning.d.ts +3 -0
  25. package/lib/components/Login/components/Section/components/Warning/warning.js +9 -0
  26. package/lib/components/Login/hooks/useUserConsent/types.d.ts +10 -0
  27. package/lib/components/Login/hooks/useUserConsent/types.js +1 -0
  28. package/lib/components/Login/hooks/useUserConsent/useUserConsent.d.ts +2 -0
  29. package/lib/components/Login/hooks/useUserConsent/useUserConsent.js +24 -0
  30. package/lib/components/Login/hooks/useUserLogin/types.d.ts +6 -0
  31. package/lib/components/Login/hooks/useUserLogin/types.js +1 -0
  32. package/lib/components/Login/hooks/useUserLogin/useUserLogin.d.ts +2 -0
  33. package/lib/components/Login/hooks/useUserLogin/useUserLogin.js +21 -0
  34. package/lib/components/common/CustomIcon/components/CloseIcon/closeIcon.d.ts +2 -0
  35. package/lib/components/common/CustomIcon/components/CloseIcon/closeIcon.js +6 -0
  36. package/lib/components/common/LoginDialog/constants.d.ts +6 -0
  37. package/lib/components/common/LoginDialog/constants.js +21 -0
  38. package/lib/components/common/LoginDialog/loginDialog.d.ts +2 -0
  39. package/lib/components/common/LoginDialog/loginDialog.js +27 -0
  40. package/lib/components/common/LoginDialog/loginDialog.styles.d.ts +3 -0
  41. package/lib/components/common/LoginDialog/loginDialog.styles.js +50 -0
  42. package/lib/components/common/LoginDialog/types.d.ts +4 -0
  43. package/lib/components/common/LoginDialog/types.js +1 -0
  44. package/lib/config/entities.d.ts +1 -0
  45. package/lib/providers/loginGuard/common/types.d.ts +18 -0
  46. package/lib/providers/loginGuard/common/types.js +1 -0
  47. package/lib/providers/loginGuard/context.d.ts +6 -0
  48. package/lib/providers/loginGuard/context.js +10 -0
  49. package/lib/providers/loginGuard/hook.d.ts +9 -0
  50. package/lib/providers/loginGuard/hook.js +12 -0
  51. package/lib/providers/loginGuard/provider.d.ts +11 -0
  52. package/lib/providers/loginGuard/provider.js +55 -0
  53. package/lib/styles/common/mui/typography.d.ts +1 -0
  54. package/lib/styles/common/mui/typography.js +7 -0
  55. package/package.json +1 -1
  56. package/src/components/Export/components/ExportForm/components/ExportButton/exportButton.tsx +8 -1
  57. package/src/components/Export/components/ManifestDownload/components/ManifestDownloadEntity/components/FileManifestDownload/fileManifestDownload.tsx +11 -3
  58. package/src/components/Index/components/AzulFileDownload/azulFileDownload.tsx +12 -5
  59. package/src/components/Layout/components/Header/components/Content/components/Navigation/components/NavigationMenu/navigationMenu.tsx +1 -1
  60. package/src/components/Layout/components/Header/components/Content/components/Navigation/components/NavigationMenuItems/navigationMenuItems.tsx +16 -15
  61. package/src/components/Layout/components/Header/components/Content/components/Navigation/constants.ts +1 -0
  62. package/src/components/Layout/components/Header/components/Content/components/Navigation/navigation.tsx +26 -18
  63. package/src/components/Layout/components/Header/header.tsx +6 -1
  64. package/src/components/Login/components/Button/types.ts +1 -1
  65. package/src/components/Login/components/Buttons/buttons.tsx +22 -0
  66. package/src/components/Login/components/Buttons/types.ts +9 -0
  67. package/src/components/Login/components/Section/components/Consent/consent.styles.ts +15 -0
  68. package/src/components/Login/components/Section/components/Consent/consent.tsx +30 -0
  69. package/src/components/Login/components/Section/components/Consent/types.ts +10 -0
  70. package/src/components/Login/components/Section/components/Warning/warning.tsx +24 -0
  71. package/src/components/Login/hooks/useUserConsent/types.ts +11 -0
  72. package/src/components/Login/hooks/useUserConsent/useUserConsent.ts +32 -0
  73. package/src/components/Login/hooks/useUserLogin/types.ts +8 -0
  74. package/src/components/Login/hooks/useUserLogin/useUserLogin.ts +29 -0
  75. package/src/components/common/CustomIcon/components/CloseIcon/closeIcon.tsx +17 -0
  76. package/src/components/common/LoginDialog/constants.ts +33 -0
  77. package/src/components/common/LoginDialog/loginDialog.styles.ts +51 -0
  78. package/src/components/common/LoginDialog/loginDialog.tsx +56 -0
  79. package/src/components/common/LoginDialog/types.ts +4 -0
  80. package/src/config/entities.ts +1 -0
  81. package/src/providers/loginGuard/common/types.ts +21 -0
  82. package/src/providers/loginGuard/context.ts +12 -0
  83. package/src/providers/loginGuard/hook.ts +14 -0
  84. package/src/providers/loginGuard/provider.tsx +76 -0
  85. package/src/styles/common/mui/typography.ts +8 -0
  86. package/tests/provider.test.tsx +191 -0
@@ -0,0 +1,29 @@
1
+ import { useCallback } from "react";
2
+ import { useAuth } from "../../../../providers/authentication/auth/hook";
3
+ import { ProviderId } from "../../../../providers/authentication/common/types";
4
+ import { useUserConsent } from "../useUserConsent/useUserConsent";
5
+ import { UseUserLogin } from "./types";
6
+
7
+ export const useUserLogin = (): UseUserLogin => {
8
+ const { service: { requestLogin } = {} } = useAuth();
9
+ const { handleConsent, handleError, state: consentState } = useUserConsent();
10
+ const { isDisabled, isError, isValid } = consentState; // Consent state: { isValid } is an indicator of whether the user has accepted the login terms.
11
+
12
+ const handleLogin = useCallback(
13
+ (providerId: ProviderId): void => {
14
+ if (!isDisabled && !isValid) {
15
+ // If the user has not accepted terms, set error state to true.
16
+ handleError(true);
17
+ return;
18
+ }
19
+ requestLogin?.(providerId);
20
+ },
21
+ [handleError, isDisabled, isValid, requestLogin]
22
+ );
23
+
24
+ return {
25
+ consentState: { isDisabled, isError },
26
+ handleConsent,
27
+ handleLogin,
28
+ };
29
+ };
@@ -0,0 +1,17 @@
1
+ import { SvgIcon, SvgIconProps } from "@mui/material";
2
+ import React from "react";
3
+
4
+ export const CloseIcon = ({
5
+ fontSize = "xsmall",
6
+ viewBox = "0 0 18 18",
7
+ ...props /* Spread props to allow for Mui SvgIconProps specific prop overrides e.g. "htmlColor". */
8
+ }: SvgIconProps): JSX.Element => {
9
+ return (
10
+ <SvgIcon fontSize={fontSize} viewBox={viewBox} {...props}>
11
+ <path
12
+ d="M8.99994 10.1061L5.38104 13.725C5.23104 13.875 5.04984 13.947 4.83744 13.941C4.62504 13.9344 4.44384 13.8561 4.29384 13.7061C4.14384 13.5561 4.06884 13.3719 4.06884 13.1535C4.06884 12.9345 4.14384 12.75 4.29384 12.6L7.89384 9.00005L4.27494 5.38115C4.12494 5.23115 4.05294 5.04695 4.05894 4.82855C4.06554 4.60955 4.14384 4.42505 4.29384 4.27505C4.44384 4.12505 4.62804 4.05005 4.84644 4.05005C5.06544 4.05005 5.24994 4.12505 5.39994 4.27505L8.99994 7.89395L12.6188 4.27505C12.7688 4.12505 12.953 4.05005 13.1714 4.05005C13.3904 4.05005 13.5749 4.12505 13.7249 4.27505C13.8749 4.42505 13.9499 4.60955 13.9499 4.82855C13.9499 5.04695 13.8749 5.23115 13.7249 5.38115L10.106 9.00005L13.7249 12.6189C13.8749 12.7689 13.9499 12.9501 13.9499 13.1625C13.9499 13.3749 13.8749 13.5561 13.7249 13.7061C13.5749 13.8561 13.3904 13.9311 13.1714 13.9311C12.953 13.9311 12.7688 13.8561 12.6188 13.7061L8.99994 10.1061Z"
13
+ fill="currentColor"
14
+ />
15
+ </SvgIcon>
16
+ );
17
+ };
@@ -0,0 +1,33 @@
1
+ import {
2
+ DialogContentTextProps,
3
+ DialogProps,
4
+ DialogTitleProps,
5
+ IconButtonProps,
6
+ IconProps,
7
+ } from "@mui/material";
8
+ import { FONT_SIZE } from "../../../styles/common/mui/icon";
9
+ import { COLOR, VARIANT } from "../../../styles/common/mui/typography";
10
+
11
+ export const DIALOG_CONTENT_TEXT_PROPS: DialogContentTextProps = {
12
+ color: COLOR.INK_LIGHT,
13
+ component: "div",
14
+ variant: VARIANT.TEXT_BODY_400,
15
+ };
16
+
17
+ export const DIALOG_PROPS: Partial<DialogProps> = {
18
+ PaperProps: { elevation: 0 },
19
+ };
20
+
21
+ export const DIALOG_TITLE_PROPS: DialogTitleProps = {
22
+ variant: VARIANT.TEXT_HEADING_SMALL,
23
+ };
24
+
25
+ export const ICON_BUTTON_PROPS: IconButtonProps = {
26
+ color: "inkLight",
27
+ edge: "end",
28
+ size: "xsmall",
29
+ };
30
+
31
+ export const ICON_PROPS: Pick<IconProps, "fontSize"> = {
32
+ fontSize: FONT_SIZE.SMALL,
33
+ };
@@ -0,0 +1,51 @@
1
+ import styled from "@emotion/styled";
2
+ import { Dialog } from "@mui/material";
3
+ import { inkMain } from "../../../styles/common/mixins/colors";
4
+ import { alpha80 } from "../../../theme/common/palette";
5
+
6
+ export const StyledDialog = styled(Dialog)`
7
+ &.MuiDialog-root {
8
+ .MuiBackdrop-root {
9
+ background-color: ${inkMain}${alpha80};
10
+ }
11
+
12
+ .MuiDialog-paper {
13
+ border-radius: 8px;
14
+ max-width: 400px;
15
+ padding: 32px;
16
+ position: relative; /* positions close icon */
17
+
18
+ .MuiDialogTitle-root,
19
+ .MuiDialogContent-root,
20
+ .MuiDialogActions-root {
21
+ padding: 0;
22
+ }
23
+
24
+ .MuiDialogTitle-root {
25
+ font-size: 20px;
26
+
27
+ .MuiIconButton-root {
28
+ position: absolute;
29
+ right: 12px;
30
+ top: 12px;
31
+ }
32
+ }
33
+
34
+ .MuiDialogContent-root {
35
+ .MuiDialogContentText-root {
36
+ margin: 8px 0;
37
+ }
38
+
39
+ .MuiGrid2-root {
40
+ margin: 24px 0;
41
+ }
42
+ }
43
+
44
+ .MuiDialogActions-root {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 16px 0;
48
+ }
49
+ }
50
+ }
51
+ `;
@@ -0,0 +1,56 @@
1
+ import {
2
+ DialogActions,
3
+ DialogContent,
4
+ DialogContentText,
5
+ DialogTitle,
6
+ IconButton,
7
+ } from "@mui/material";
8
+ import React from "react";
9
+ import { useAuthenticationConfig } from "../../../hooks/authentication/config/useAuthenticationConfig";
10
+ import { Buttons } from "../../Login/components/Buttons/buttons";
11
+ import { Consent } from "../../Login/components/Section/components/Consent/consent";
12
+ import { Warning } from "../../Login/components/Section/components/Warning/warning";
13
+ import { useUserLogin } from "../../Login/hooks/useUserLogin/useUserLogin";
14
+ import { CloseIcon } from "../CustomIcon/components/CloseIcon/closeIcon";
15
+ import {
16
+ DIALOG_CONTENT_TEXT_PROPS,
17
+ DIALOG_PROPS,
18
+ DIALOG_TITLE_PROPS,
19
+ ICON_BUTTON_PROPS,
20
+ ICON_PROPS,
21
+ } from "./constants";
22
+ import { StyledDialog } from "./loginDialog.styles";
23
+ import { LoginDialogProps } from "./types";
24
+
25
+ export const LoginDialog = ({
26
+ onClose,
27
+ open,
28
+ }: LoginDialogProps): JSX.Element | null => {
29
+ const authConfig = useAuthenticationConfig();
30
+ const { consentState, handleConsent, handleLogin } = useUserLogin();
31
+
32
+ if (!authConfig) return null;
33
+
34
+ return (
35
+ <StyledDialog {...DIALOG_PROPS} onClose={onClose} open={open}>
36
+ <DialogTitle {...DIALOG_TITLE_PROPS}>
37
+ <div>Sign In Required</div>
38
+ <IconButton {...ICON_BUTTON_PROPS} onClick={onClose}>
39
+ <CloseIcon {...ICON_PROPS} />
40
+ </IconButton>
41
+ </DialogTitle>
42
+ <DialogContent>
43
+ <DialogContentText {...DIALOG_CONTENT_TEXT_PROPS}>
44
+ Please sign in to proceed with this action.
45
+ </DialogContentText>
46
+ <Consent handleConsent={handleConsent} {...consentState}>
47
+ {authConfig.termsOfService}
48
+ </Consent>
49
+ </DialogContent>
50
+ <DialogActions disableSpacing>
51
+ <Buttons handleLogin={handleLogin} providers={authConfig.providers} />
52
+ </DialogActions>
53
+ <Warning>{authConfig.warning}</Warning>
54
+ </StyledDialog>
55
+ );
56
+ };
@@ -0,0 +1,4 @@
1
+ export interface LoginDialogProps {
2
+ onClose: () => void;
3
+ open: boolean;
4
+ }
@@ -379,6 +379,7 @@ export interface SiteConfig {
379
379
  entities: EntityConfig[];
380
380
  explorerTitle: HeroTitle;
381
381
  export?: ExportConfig;
382
+ exportsRequireAuth?: boolean;
382
383
  exportToTerraUrl?: string; // TODO(cc) revist location; possibly nest inside "export"?
383
384
  gitHubUrl?: string;
384
385
  layout: {
@@ -0,0 +1,21 @@
1
+ import { ReactNode } from "react";
2
+
3
+ /**
4
+ * A callback function to be stored and then executed upon successful login.
5
+ */
6
+ export type LoginGuardCallback = () => void;
7
+
8
+ /**
9
+ * The shape of the LoginGuard context, provides a function to trigger the
10
+ * login process.
11
+ */
12
+ export interface LoginGuardContextProps {
13
+ requireLogin: (callback?: LoginGuardCallback) => void;
14
+ }
15
+
16
+ /**
17
+ * The properties for the LoginGuardProvider component.
18
+ */
19
+ export interface LoginGuardProviderProps {
20
+ children: ReactNode;
21
+ }
@@ -0,0 +1,12 @@
1
+ import { createContext } from "react";
2
+ import { LoginGuardCallback, LoginGuardContextProps } from "./common/types";
3
+
4
+ /**
5
+ * LoginGuardContext provides a way to trigger a login process. Default value is to
6
+ * call the callback immediately, if specified.
7
+ */
8
+ export const LoginGuardContext = createContext<LoginGuardContextProps>({
9
+ requireLogin: (callback?: LoginGuardCallback) => {
10
+ callback?.();
11
+ },
12
+ });
@@ -0,0 +1,14 @@
1
+ import { useContext } from "react";
2
+ import { LoginGuardContextProps } from "./common/types";
3
+ import { LoginGuardContext } from "./context";
4
+
5
+ /**
6
+ * Custom hook to access the LoginGuard context. This hook returns an object
7
+ * containing the "requireLogin" function, which allows triggering the application's
8
+ * login process.
9
+ *
10
+ * @returns The current LoginGuard context value.
11
+ */
12
+ export function useLoginGuard(): LoginGuardContextProps {
13
+ return useContext<LoginGuardContextProps>(LoginGuardContext);
14
+ }
@@ -0,0 +1,76 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from "react";
2
+ import { LoginDialog } from "../../components/common/LoginDialog/loginDialog";
3
+ import { useAuthenticationConfig } from "../../hooks/authentication/config/useAuthenticationConfig";
4
+ import { useConfig } from "../../hooks/useConfig";
5
+ import { useAuth } from "../authentication/auth/hook";
6
+ import { LoginGuardCallback, LoginGuardProviderProps } from "./common/types";
7
+ import { LoginGuardContext } from "./context";
8
+
9
+ /**
10
+ * LoginGuardProvider is responsible for intercepting actions that require user authentication.
11
+ * It provides a "requireLogin" function via context. When a protected action is triggered while the
12
+ * user is unauthenticated, the LoginDialog is displayed. Upon successful authentication, the saved
13
+ * callback is invoked.
14
+ *
15
+ * @param {LoginGuardProviderProps} props - The provider props that include children.
16
+ * @returns The provider component.
17
+ */
18
+ export function LoginGuardProvider({
19
+ children,
20
+ }: LoginGuardProviderProps): JSX.Element {
21
+ // Dialog open state.
22
+ const [open, setOpen] = useState(false);
23
+
24
+ // Use ref to store the callback without triggering re-render.
25
+ const callbackRef = useRef<LoginGuardCallback | undefined>(undefined);
26
+
27
+ // Determine if authentication is enabled.
28
+ const authConfig = useAuthenticationConfig();
29
+
30
+ // Determine if authentication is required for downloads and exports.
31
+ const {
32
+ config: { exportsRequireAuth },
33
+ } = useConfig();
34
+
35
+ // Get the user's authenticated state.
36
+ const {
37
+ authState: { isAuthenticated },
38
+ } = useAuth();
39
+
40
+ // If the user authenticates, close dialog then fire and clear callback.
41
+ useEffect(() => {
42
+ if (isAuthenticated) {
43
+ setOpen(false);
44
+ callbackRef.current?.();
45
+ // Clear callback after firing.
46
+ callbackRef.current = undefined;
47
+ }
48
+ }, [isAuthenticated]);
49
+
50
+ // Handler to close the dialog.
51
+ const onClose = useCallback(() => {
52
+ setOpen(false);
53
+ // Clear any stored callback.
54
+ callbackRef.current = undefined;
55
+ }, []);
56
+
57
+ // Block actions that require authentication, or fire callback if already authenticated.
58
+ const requireLogin = useCallback(
59
+ (cb?: LoginGuardCallback) => {
60
+ if (authConfig && exportsRequireAuth && !isAuthenticated) {
61
+ callbackRef.current = cb;
62
+ setOpen(true);
63
+ } else {
64
+ cb?.();
65
+ }
66
+ },
67
+ [authConfig, exportsRequireAuth, isAuthenticated]
68
+ );
69
+
70
+ return (
71
+ <LoginGuardContext.Provider value={{ requireLogin }}>
72
+ {children}
73
+ <LoginDialog open={open} onClose={onClose} />
74
+ </LoginGuardContext.Provider>
75
+ );
76
+ }
@@ -1,5 +1,13 @@
1
1
  import { TypographyOwnProps } from "@mui/material";
2
2
 
3
+ export const COLOR: Record<string, TypographyOwnProps["color"]> = {
4
+ INHERIT: "inherit",
5
+ INK_LIGHT: "ink.light",
6
+ INK_MAIN: "ink.main",
7
+ };
8
+
3
9
  export const VARIANT: Record<string, TypographyOwnProps["variant"]> = {
4
10
  INHERIT: "inherit",
11
+ TEXT_BODY_400: "text-body-400",
12
+ TEXT_HEADING_SMALL: "text-heading-small",
5
13
  };
@@ -0,0 +1,191 @@
1
+ import { jest } from "@jest/globals";
2
+ import { act, render, screen } from "@testing-library/react";
3
+ import React from "react";
4
+ import { LoginGuardContext } from "../src/providers/loginGuard/context";
5
+
6
+ jest.unstable_mockModule("../src/hooks/useConfig", () => ({
7
+ useConfig: jest.fn(),
8
+ }));
9
+
10
+ jest.unstable_mockModule("../src/providers/authentication/auth/hook", () => ({
11
+ useAuth: jest.fn(),
12
+ }));
13
+
14
+ jest.unstable_mockModule(
15
+ "../src/hooks/authentication/config/useAuthenticationConfig",
16
+ () => ({
17
+ useAuthenticationConfig: jest.fn(),
18
+ })
19
+ );
20
+
21
+ const TEST_ID_LOGIN_DIALOG = "login-dialog";
22
+ const TEXT_DIALOG_CLOSED = "closed";
23
+ const TEXT_DIALOG_OPEN = "open";
24
+ jest.unstable_mockModule(
25
+ "../src/components/common/LoginDialog/loginDialog",
26
+ () => ({
27
+ LoginDialog: ({ open }: { open: boolean }): JSX.Element => (
28
+ <div data-testid={TEST_ID_LOGIN_DIALOG}>
29
+ {open ? TEXT_DIALOG_OPEN : TEXT_DIALOG_CLOSED}
30
+ </div>
31
+ ),
32
+ })
33
+ );
34
+
35
+ const { useConfig } = await import("../src/hooks/useConfig");
36
+ const { useAuth } = await import("../src/providers/authentication/auth/hook");
37
+ const { useAuthenticationConfig } = await import(
38
+ "../src/hooks/authentication/config/useAuthenticationConfig"
39
+ );
40
+
41
+ const { LoginGuardProvider } = await import(
42
+ "../src/providers/loginGuard/provider"
43
+ );
44
+
45
+ const TEXT_BUTTON_EXPORT = "export";
46
+
47
+ describe("LoginGuardProvider", () => {
48
+ beforeEach(() => {
49
+ // Mock hooks used by login guard.
50
+ (useConfig as jest.Mock).mockReturnValue({
51
+ config: {
52
+ exportsRequireAuth: true,
53
+ },
54
+ });
55
+ (useAuth as jest.Mock).mockReturnValue({
56
+ authState: {
57
+ isAuthenticated: false,
58
+ },
59
+ });
60
+ (useAuthenticationConfig as jest.Mock).mockReturnValue({});
61
+ });
62
+
63
+ it("should render children and login dialog closed", () => {
64
+ render(
65
+ <LoginGuardProvider>
66
+ <div data-testid="child">child component</div>
67
+ </LoginGuardProvider>
68
+ );
69
+
70
+ expect(screen.getByTestId("child")).toBeTruthy();
71
+ expect(screen.getByTestId(TEST_ID_LOGIN_DIALOG).textContent).toBe(
72
+ TEXT_DIALOG_CLOSED
73
+ );
74
+ });
75
+
76
+ it("calls callback immediately if user is authenticated", () => {
77
+ const callback = jest.fn();
78
+
79
+ // Simulate user authentication.
80
+ (useAuth as jest.Mock).mockReturnValue({
81
+ authState: { isAuthenticated: true },
82
+ });
83
+
84
+ render(
85
+ <LoginGuardProvider>
86
+ <LoginGuardContext.Consumer>
87
+ {({ requireLogin }) => (
88
+ <button onClick={() => requireLogin(callback)}>
89
+ {TEXT_BUTTON_EXPORT}
90
+ </button>
91
+ )}
92
+ </LoginGuardContext.Consumer>
93
+ </LoginGuardProvider>
94
+ );
95
+
96
+ // Click button requiring login.
97
+ act(() => {
98
+ screen.getByText(TEXT_BUTTON_EXPORT).click();
99
+ });
100
+
101
+ // User is authenticated; callback should be fired immediately.
102
+ expect(callback).toHaveBeenCalled();
103
+
104
+ // Login dialog should not be open.
105
+ expect(screen.getByTestId(TEST_ID_LOGIN_DIALOG).textContent).toBe(
106
+ TEXT_DIALOG_CLOSED
107
+ );
108
+ });
109
+
110
+ it("calls callback immediately if exportsRequireAuth is false", () => {
111
+ const callback = jest.fn();
112
+
113
+ // Simulate exportsRequireAuth being false.
114
+ (useConfig as jest.Mock).mockReturnValue({
115
+ config: {
116
+ exportsRequireAuth: false,
117
+ },
118
+ });
119
+
120
+ render(
121
+ <LoginGuardProvider>
122
+ <LoginGuardContext.Consumer>
123
+ {({ requireLogin }) => (
124
+ <button onClick={() => requireLogin(callback)}>
125
+ {TEXT_BUTTON_EXPORT}
126
+ </button>
127
+ )}
128
+ </LoginGuardContext.Consumer>
129
+ </LoginGuardProvider>
130
+ );
131
+
132
+ // Click button requiring login.
133
+ act(() => {
134
+ screen.getByText(TEXT_BUTTON_EXPORT).click();
135
+ });
136
+
137
+ // exportsRequireAuth is false; callback should be fired immediately.
138
+ expect(callback).toHaveBeenCalled();
139
+
140
+ // Login dialog should not be open.
141
+ expect(screen.getByTestId(TEST_ID_LOGIN_DIALOG).textContent).toBe(
142
+ TEXT_DIALOG_CLOSED
143
+ );
144
+ });
145
+
146
+ it("should call callback after user authenticates", async () => {
147
+ const callback = jest.fn();
148
+
149
+ const { rerender } = render(
150
+ <LoginGuardProvider>
151
+ <LoginGuardContext.Consumer>
152
+ {({ requireLogin }) => (
153
+ <button onClick={() => requireLogin(callback)}>
154
+ {TEXT_BUTTON_EXPORT}
155
+ </button>
156
+ )}
157
+ </LoginGuardContext.Consumer>
158
+ </LoginGuardProvider>
159
+ );
160
+
161
+ // Click button requiring login.
162
+ act(() => {
163
+ screen.getByText(TEXT_BUTTON_EXPORT).click();
164
+ });
165
+
166
+ // User is not authenticated; callback should not have been called.
167
+ expect(callback).not.toHaveBeenCalled();
168
+
169
+ // User is not authenticated; login dialog should be open.
170
+ expect(screen.getByTestId(TEST_ID_LOGIN_DIALOG).textContent).toBe(
171
+ TEXT_DIALOG_OPEN
172
+ );
173
+
174
+ // Simulate user authentication.
175
+ await act(async () => {
176
+ (useAuth as jest.Mock).mockReturnValue({
177
+ authState: { isAuthenticated: true },
178
+ });
179
+ });
180
+
181
+ // Rerender to trigger useEffect.
182
+ rerender(
183
+ <LoginGuardProvider>
184
+ <div />
185
+ </LoginGuardProvider>
186
+ );
187
+
188
+ // Callback should be called (in useEffect called on re-render).
189
+ expect(callback).toHaveBeenCalled();
190
+ });
191
+ });