@pautena/react-design-system 0.1.2 → 0.2.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 (191) hide show
  1. package/README.md +4 -0
  2. package/dist/cjs/index.js +4 -259
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/types/generators/model-router/screens/add-screen.d.ts +1 -1
  5. package/dist/cjs/types/generators/model-router/screens/list-screen.d.ts +1 -1
  6. package/dist/cjs/types/generators/model-router/screens/screens.types.d.ts +20 -0
  7. package/dist/cjs/types/generators/model-router/screens/update-screen.d.ts +1 -1
  8. package/dist/cjs/types/index.d.ts +1 -0
  9. package/dist/esm/index.js +4 -259
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/types/generators/model-router/screens/add-screen.d.ts +1 -1
  12. package/dist/esm/types/generators/model-router/screens/list-screen.d.ts +1 -1
  13. package/dist/esm/types/generators/model-router/screens/screens.types.d.ts +20 -0
  14. package/dist/esm/types/generators/model-router/screens/update-screen.d.ts +1 -1
  15. package/dist/esm/types/index.d.ts +1 -0
  16. package/dist/index.d.ts +52 -3
  17. package/package.json +13 -2
  18. package/src/components/app-bar/app-bar.stories.tsx +54 -0
  19. package/src/components/app-bar/app-bar.test.tsx +142 -0
  20. package/src/components/app-bar/app-bar.tsx +150 -0
  21. package/src/components/app-bar/app-bar.types.ts +17 -0
  22. package/src/components/app-bar/index.ts +3 -0
  23. package/src/components/app-bar/mini-app-bar/index.ts +1 -0
  24. package/src/components/app-bar/mini-app-bar/mini-app-bar.tsx +31 -0
  25. package/src/components/bullet/bullet.stories.tsx +43 -0
  26. package/src/components/bullet/bullet.test.tsx +24 -0
  27. package/src/components/bullet/bullet.tsx +30 -0
  28. package/src/components/bullet/index.ts +1 -0
  29. package/src/components/center-container/center-container.stories.tsx +50 -0
  30. package/src/components/center-container/center-container.test.tsx +16 -0
  31. package/src/components/center-container/center-container.tsx +32 -0
  32. package/src/components/center-container/index.ts +1 -0
  33. package/src/components/content/content.stories.tsx +23 -0
  34. package/src/components/content/content.test.tsx +26 -0
  35. package/src/components/content/content.tsx +11 -0
  36. package/src/components/content/content.types.ts +5 -0
  37. package/src/components/content/index.ts +2 -0
  38. package/src/components/drawer/__snapshots__/drawer.test.tsx.snap +20 -0
  39. package/src/components/drawer/drawer.context.ts +20 -0
  40. package/src/components/drawer/drawer.mixins.ts +24 -0
  41. package/src/components/drawer/drawer.mock.tsx +100 -0
  42. package/src/components/drawer/drawer.provider.tsx +23 -0
  43. package/src/components/drawer/drawer.test.tsx +97 -0
  44. package/src/components/drawer/drawer.tsx +30 -0
  45. package/src/components/drawer/drawer.types.ts +53 -0
  46. package/src/components/drawer/index.ts +5 -0
  47. package/src/components/drawer/mini-drawer/index.ts +1 -0
  48. package/src/components/drawer/mini-drawer/mini-drawer.stories.tsx +34 -0
  49. package/src/components/drawer/mini-drawer/mini-drawer.tsx +67 -0
  50. package/src/components/drawer-content/drawer-content.stories.tsx +29 -0
  51. package/src/components/drawer-content/drawer-content.test.tsx +34 -0
  52. package/src/components/drawer-content/drawer-content.tsx +18 -0
  53. package/src/components/drawer-content/index.ts +1 -0
  54. package/src/components/drawer-item/drawer-item.stories.tsx +62 -0
  55. package/src/components/drawer-item/drawer-item.test.tsx +119 -0
  56. package/src/components/drawer-item/drawer-item.tsx +71 -0
  57. package/src/components/drawer-item/index.ts +1 -0
  58. package/src/components/drawer-section/drawer-section.mock.tsx +39 -0
  59. package/src/components/drawer-section/drawer-section.stories.tsx +28 -0
  60. package/src/components/drawer-section/drawer-section.test.tsx +44 -0
  61. package/src/components/drawer-section/drawer-section.tsx +40 -0
  62. package/src/components/drawer-section/index.ts +1 -0
  63. package/src/components/header/header.dummy.ts +55 -0
  64. package/src/components/header/header.stories.tsx +116 -0
  65. package/src/components/header/header.test.tsx +159 -0
  66. package/src/components/header/header.tsx +121 -0
  67. package/src/components/header/header.types.ts +61 -0
  68. package/src/components/header/index.ts +2 -0
  69. package/src/components/index.ts +18 -0
  70. package/src/components/label/index.ts +1 -0
  71. package/src/components/label/label.stories.tsx +49 -0
  72. package/src/components/label/label.test.tsx +30 -0
  73. package/src/components/label/label.tsx +60 -0
  74. package/src/components/link/index.ts +1 -0
  75. package/src/components/link/link.tsx +17 -0
  76. package/src/components/loading-area/index.ts +1 -0
  77. package/src/components/loading-area/loading-area.stories.tsx +17 -0
  78. package/src/components/loading-area/loading-area.test.tsx +11 -0
  79. package/src/components/loading-area/loading-area.tsx +13 -0
  80. package/src/components/placeholder/index.ts +1 -0
  81. package/src/components/placeholder/placeholder.mock.ts +15 -0
  82. package/src/components/placeholder/placeholder.stories.tsx +44 -0
  83. package/src/components/placeholder/placeholder.test.tsx +76 -0
  84. package/src/components/placeholder/placeholder.tsx +75 -0
  85. package/src/components/query-container/index.ts +1 -0
  86. package/src/components/query-container/query-container.stories.tsx +68 -0
  87. package/src/components/query-container/query-container.test.tsx +95 -0
  88. package/src/components/query-container/query-container.tsx +71 -0
  89. package/src/components/sign-in/index.ts +1 -0
  90. package/src/components/sign-in/sign-in.stories.tsx +36 -0
  91. package/src/components/sign-in/sign-in.test.tsx +95 -0
  92. package/src/components/sign-in/sign-in.tsx +97 -0
  93. package/src/components/tab/index.ts +2 -0
  94. package/src/components/tab/tab-card/index.ts +1 -0
  95. package/src/components/tab/tab-card/tab-card.dummy.tsx +30 -0
  96. package/src/components/tab/tab-card/tab-card.stories.tsx +22 -0
  97. package/src/components/tab/tab-card/tab-card.test.tsx +53 -0
  98. package/src/components/tab/tab-card/tab-card.tsx +27 -0
  99. package/src/components/tab/tab-panel/index.ts +1 -0
  100. package/src/components/tab/tab-panel/tab-panel.test.tsx +26 -0
  101. package/src/components/tab/tab-panel/tab-panel.tsx +27 -0
  102. package/src/components/table/enhanced-remote-table/enhanced-remote-table.mock.tsx +27 -0
  103. package/src/components/table/enhanced-remote-table/enhanced-remote-table.stories.tsx +24 -0
  104. package/src/components/table/enhanced-remote-table/enhanced-remote-table.test.tsx +77 -0
  105. package/src/components/table/enhanced-remote-table/enhanced-remote-table.tsx +74 -0
  106. package/src/components/table/enhanced-remote-table/index.ts +1 -0
  107. package/src/components/table/enhanced-table/enhanced-table-head.tsx +58 -0
  108. package/src/components/table/enhanced-table/enhanced-table.mock.tsx +93 -0
  109. package/src/components/table/enhanced-table/enhanced-table.stories.tsx +21 -0
  110. package/src/components/table/enhanced-table/enhanced-table.test.tsx +107 -0
  111. package/src/components/table/enhanced-table/enhanced-table.tsx +136 -0
  112. package/src/components/table/enhanced-table/index.ts +2 -0
  113. package/src/components/table/index.ts +2 -0
  114. package/src/components/table-list/index.ts +1 -0
  115. package/src/components/table-list/table-list.stories.tsx +75 -0
  116. package/src/components/table-list/table-list.test.tsx +291 -0
  117. package/src/components/table-list/table-list.tsx +127 -0
  118. package/src/components/value-displays/group-value-card/group-value-card.mock.tsx +35 -0
  119. package/src/components/value-displays/group-value-card/group-value-card.stories.tsx +26 -0
  120. package/src/components/value-displays/group-value-card/group-value-card.test.tsx +58 -0
  121. package/src/components/value-displays/group-value-card/group-value-card.tsx +63 -0
  122. package/src/components/value-displays/group-value-card/index.ts +1 -0
  123. package/src/components/value-displays/index.ts +4 -0
  124. package/src/components/value-displays/value-boolean/index.ts +1 -0
  125. package/src/components/value-displays/value-boolean/value-boolean.stories.tsx +25 -0
  126. package/src/components/value-displays/value-boolean/value-boolean.test.tsx +27 -0
  127. package/src/components/value-displays/value-boolean/value-boolean.tsx +33 -0
  128. package/src/components/value-displays/value-card/index.ts +1 -0
  129. package/src/components/value-displays/value-card/value-card.stories.tsx +22 -0
  130. package/src/components/value-displays/value-card/value-card.test.tsx +18 -0
  131. package/src/components/value-displays/value-card/value-card.tsx +12 -0
  132. package/src/components/value-displays/value-text/index.ts +1 -0
  133. package/src/components/value-displays/value-text/value-test.test.tsx +21 -0
  134. package/src/components/value-displays/value-text/value-text.stories.tsx +26 -0
  135. package/src/components/value-displays/value-text/value-text.tsx +32 -0
  136. package/src/generators/generators.mock.ts +238 -0
  137. package/src/generators/generators.model.ts +46 -0
  138. package/src/generators/index.ts +4 -0
  139. package/src/generators/model-form/index.ts +1 -0
  140. package/src/generators/model-form/model-form.stories.tsx +30 -0
  141. package/src/generators/model-form/model-form.test.tsx +100 -0
  142. package/src/generators/model-form/model-form.tsx +97 -0
  143. package/src/generators/model-router/index.ts +1 -0
  144. package/src/generators/model-router/model-router.test.tsx +831 -0
  145. package/src/generators/model-router/model-router.tsx +30 -0
  146. package/src/generators/model-router/model-router.types.ts +14 -0
  147. package/src/generators/model-router/screens/add-screen.tsx +70 -0
  148. package/src/generators/model-router/screens/details-screen.tsx +62 -0
  149. package/src/generators/model-router/screens/index.ts +4 -0
  150. package/src/generators/model-router/screens/list-screen.tsx +125 -0
  151. package/src/generators/model-router/screens/screens.types.ts +38 -0
  152. package/src/generators/model-router/screens/update-screen.tsx +97 -0
  153. package/src/generators/model-router/stories/details-screen.stories.tsx +38 -0
  154. package/src/generators/model-router/stories/list-screen.stories.tsx +96 -0
  155. package/src/generators/model-router/stories/model-router.stories.tsx +176 -0
  156. package/src/generators/model-router/stories/templates.tsx +39 -0
  157. package/src/generators/object-details/index.ts +1 -0
  158. package/src/generators/object-details/object-details.stories.tsx +20 -0
  159. package/src/generators/object-details/object-details.test.tsx +21 -0
  160. package/src/generators/object-details/object-details.tsx +76 -0
  161. package/src/index.ts +5 -0
  162. package/src/layouts/app-bar-with-drawer-layout/app-bar-with-drawer-layout.stories.tsx +28 -0
  163. package/src/layouts/app-bar-with-drawer-layout/app-bar-with-drawer-layout.test.tsx +30 -0
  164. package/src/layouts/app-bar-with-drawer-layout/app-bar-with-drawer-layout.tsx +37 -0
  165. package/src/layouts/app-bar-with-drawer-layout/index.ts +1 -0
  166. package/src/layouts/header-layout/header-layout.stories.tsx +204 -0
  167. package/src/layouts/header-layout/header-layout.test.tsx +37 -0
  168. package/src/layouts/header-layout/header-layout.tsx +23 -0
  169. package/src/layouts/header-layout/index.ts +1 -0
  170. package/src/layouts/index.ts +2 -0
  171. package/src/providers/index.ts +2 -0
  172. package/src/providers/notification-center/index.ts +2 -0
  173. package/src/providers/notification-center/notification-center.context.ts +37 -0
  174. package/src/providers/notification-center/notification-center.provider.tsx +51 -0
  175. package/src/providers/notification-center/notification-center.stories.tsx +52 -0
  176. package/src/providers/notification-center/notification-center.test.tsx +112 -0
  177. package/src/providers/tab-provider/index.ts +2 -0
  178. package/src/providers/tab-provider/tab-provider.context.ts +8 -0
  179. package/src/providers/tab-provider/tab-provider.provider.tsx +13 -0
  180. package/src/storybook.tsx +90 -0
  181. package/src/tests/assertions.ts +76 -0
  182. package/src/tests/components.tsx +60 -0
  183. package/src/tests/content-placeholder.stories.tsx +16 -0
  184. package/src/tests/index.ts +3 -0
  185. package/src/tests/skeleton-card.stories.tsx +18 -0
  186. package/src/tests/testing-library.tsx +65 -0
  187. package/src/utils/arrays.test.ts +9 -0
  188. package/src/utils/arrays.ts +7 -0
  189. package/src/utils/index.ts +2 -0
  190. package/src/utils/theme.ts +11 -0
  191. package/.prettierrc.js +0 -5
@@ -0,0 +1,23 @@
1
+ import { Box } from "@mui/material";
2
+ import React from "react";
3
+ import { ContentElement, HeaderElement } from "../../components";
4
+ import { LoadingArea } from "../../components/loading-area";
5
+ import { TabProvider } from "../../providers";
6
+
7
+ interface HeaderLayoutProps {
8
+ loading?: boolean;
9
+ children: [HeaderElement, ContentElement];
10
+ }
11
+
12
+ export const HeaderLayout = ({ loading, children }: HeaderLayoutProps) => {
13
+ const [headerElement, contentElement] = children;
14
+
15
+ return (
16
+ <TabProvider>
17
+ <Box display="flex" flexDirection="column" height={1}>
18
+ {headerElement}
19
+ {loading ? <LoadingArea /> : contentElement}
20
+ </Box>
21
+ </TabProvider>
22
+ );
23
+ };
@@ -0,0 +1 @@
1
+ export * from "./header-layout";
@@ -0,0 +1,2 @@
1
+ export * from "./app-bar-with-drawer-layout";
2
+ export * from "./header-layout";
@@ -0,0 +1,2 @@
1
+ export * from "./notification-center";
2
+ export * from "./tab-provider";
@@ -0,0 +1,2 @@
1
+ export * from "./notification-center.provider";
2
+ export * from "./notification-center.context";
@@ -0,0 +1,37 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { AlertColor } from "@mui/material";
3
+ import React from "react";
4
+
5
+ export const NotificationCenterProviderUndefinedError = new Error(
6
+ "NotificationCenterContext.Provider is required and was undefined",
7
+ );
8
+ export type SnackbarContentType =
9
+ | React.ReactElement<any, any>
10
+ | ((key: string) => React.ReactElement<any, any>);
11
+
12
+ export type SnackbarActionType = React.ReactNode | ((key: string) => React.ReactNode);
13
+
14
+ export interface Notification {
15
+ severity: AlertColor;
16
+ title?: string;
17
+ message: string;
18
+ }
19
+
20
+ export interface NotificationCenterProps {
21
+ show(notification: Notification): void;
22
+ hide(): void;
23
+ }
24
+
25
+ export const NotificationCenterContext = React.createContext<NotificationCenterProps | undefined>(
26
+ undefined,
27
+ );
28
+
29
+ export const useNotificationCenter = () => {
30
+ const context = React.useContext(NotificationCenterContext);
31
+
32
+ if (context === undefined) {
33
+ throw NotificationCenterProviderUndefinedError;
34
+ }
35
+
36
+ return context;
37
+ };
@@ -0,0 +1,51 @@
1
+ import { useState } from "react";
2
+ import { Snackbar, Alert, AlertTitle } from "@mui/material";
3
+ import React, { PropsWithChildren } from "react";
4
+ import { Notification, NotificationCenterContext } from "./notification-center.context";
5
+
6
+ export type NotificationCenterProviderProps = PropsWithChildren<{
7
+ autoHideDuration?: number;
8
+ }>;
9
+
10
+ export const NotificationCenterProvider = ({
11
+ children,
12
+ autoHideDuration = 6000,
13
+ }: NotificationCenterProviderProps) => {
14
+ const [notification, setNotification] = useState<Notification | undefined>(undefined);
15
+ const [open, setOpen] = useState(false);
16
+ const show = (notification: Notification) => {
17
+ setNotification(notification);
18
+ setOpen(true);
19
+ };
20
+
21
+ const hide = () => {
22
+ setOpen(false);
23
+ };
24
+
25
+ return (
26
+ <NotificationCenterContext.Provider
27
+ value={{
28
+ show,
29
+ hide,
30
+ }}
31
+ >
32
+ <Snackbar
33
+ open={open}
34
+ autoHideDuration={autoHideDuration}
35
+ onClose={hide}
36
+ anchorOrigin={{ vertical: "top", horizontal: "right" }}
37
+ >
38
+ <Alert
39
+ onClose={hide}
40
+ severity={notification?.severity}
41
+ aria-label={notification?.severity}
42
+ sx={{ width: "100%" }}
43
+ >
44
+ {notification?.title && <AlertTitle>{notification?.title}</AlertTitle>}
45
+ {notification?.message}
46
+ </Alert>
47
+ </Snackbar>
48
+ {children}
49
+ </NotificationCenterContext.Provider>
50
+ );
51
+ };
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { ComponentMeta } from "@storybook/react";
3
+ import { NotificationCenterProvider } from "./notification-center.provider";
4
+ import { useNotificationCenter } from "./notification-center.context";
5
+ import { ContentPlaceholder } from "../../tests";
6
+ import { Button, Box } from "@mui/material";
7
+
8
+ const DummyError = {
9
+ title: "Internal Server error",
10
+ message: "Unable to save the item",
11
+ };
12
+
13
+ export default {
14
+ title: "Providers/NotificationCenter",
15
+ component: ContentPlaceholder,
16
+ decorators: [
17
+ (Story) => (
18
+ <NotificationCenterProvider>
19
+ <Story />
20
+ </NotificationCenterProvider>
21
+ ),
22
+ ],
23
+ parameters: {
24
+ layout: "fullscreen",
25
+ },
26
+ } as ComponentMeta<typeof ContentPlaceholder>;
27
+
28
+ export const Default = () => {
29
+ const { show, hide } = useNotificationCenter();
30
+
31
+ return (
32
+ <ContentPlaceholder size={3} p={2}>
33
+ <Box pb={2}>
34
+ <Button onClick={() => show({ ...DummyError, severity: "info" })} variant="contained">
35
+ Show info
36
+ </Button>
37
+ <Button onClick={() => show({ ...DummyError, severity: "success" })} variant="contained">
38
+ Show success
39
+ </Button>
40
+ <Button onClick={() => show({ ...DummyError, severity: "warning" })} variant="contained">
41
+ Show warning
42
+ </Button>
43
+ <Button onClick={() => show({ ...DummyError, severity: "error" })} variant="contained">
44
+ Show error
45
+ </Button>
46
+ <Button onClick={hide} variant="contained">
47
+ hide
48
+ </Button>
49
+ </Box>
50
+ </ContentPlaceholder>
51
+ );
52
+ };
@@ -0,0 +1,112 @@
1
+ import React from "react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { AlertColor, Box, Button } from "@mui/material";
4
+ import { render, screen, waitForElementToBeRemoved, expectAlert } from "../../tests";
5
+ import { NotificationCenterProvider } from "./notification-center.provider";
6
+ import {
7
+ NotificationCenterProviderUndefinedError,
8
+ useNotificationCenter,
9
+ } from "./notification-center.context";
10
+
11
+ describe("NotificationCenterProvider", () => {
12
+ const renderComponent = ({ autoHideDuration }: { autoHideDuration?: number } = {}) => {
13
+ const TestContent = () => {
14
+ const { show, hide } = useNotificationCenter();
15
+
16
+ const showNotification = (severity: AlertColor) => {
17
+ show({
18
+ title: "An alert!",
19
+ message: "Lorem ipsum sit amet",
20
+ severity,
21
+ });
22
+ };
23
+ return (
24
+ <>
25
+ <Button onClick={() => showNotification("success")}>success</Button>
26
+ <Button onClick={() => showNotification("info")}>info</Button>
27
+ <Button onClick={() => showNotification("warning")}>warning</Button>
28
+ <Button onClick={() => showNotification("error")}>error</Button>
29
+ <Button onClick={hide}>hide</Button>
30
+ </>
31
+ );
32
+ };
33
+
34
+ return render(
35
+ <NotificationCenterProvider autoHideDuration={autoHideDuration}>
36
+ <TestContent />
37
+ </NotificationCenterProvider>,
38
+ );
39
+ };
40
+
41
+ it("would render an alert with a title and message when a notification is requested", async () => {
42
+ renderComponent();
43
+
44
+ await userEvent.click(screen.getByRole("button", { name: /info/i }));
45
+
46
+ await expectAlert({
47
+ title: /an alert/i,
48
+ message: /lorem ipsum sit amet/i,
49
+ severity: "info",
50
+ });
51
+ });
52
+
53
+ it("would hide the notification if close is called", async () => {
54
+ renderComponent();
55
+
56
+ await userEvent.click(screen.getByRole("button", { name: /info/i }));
57
+ await userEvent.click(screen.getByRole("button", { name: /hide/i }));
58
+
59
+ await waitForElementToBeRemoved(() => screen.getByRole("alert"));
60
+ });
61
+
62
+ it("would hide the notification if the user clicks the close button", async () => {
63
+ renderComponent();
64
+
65
+ await userEvent.click(screen.getByRole("button", { name: /info/i }));
66
+ await userEvent.click(screen.getByTestId("CloseIcon"));
67
+
68
+ await waitForElementToBeRemoved(() => screen.getByRole("alert"));
69
+ });
70
+
71
+ it("would hide the notification after a delay", async () => {
72
+ renderComponent({ autoHideDuration: 100 });
73
+
74
+ await userEvent.click(screen.getByRole("button", { name: /info/i }));
75
+
76
+ await waitForElementToBeRemoved(() => screen.getByRole("alert"));
77
+ });
78
+
79
+ describe("notification severity", () => {
80
+ it.each([
81
+ ["success", /success/i],
82
+ ["info", /info/i],
83
+ ["warning", /warning/i],
84
+ ["error", /error/i],
85
+ ])(
86
+ "would render a notification with severity %s",
87
+ async (severity: string, buttonName: RegExp) => {
88
+ renderComponent();
89
+
90
+ await userEvent.click(screen.getByRole("button", { name: buttonName }));
91
+
92
+ expect(screen.getByRole("alert", { name: severity })).toBeInTheDocument();
93
+ },
94
+ );
95
+ });
96
+ });
97
+
98
+ describe("useNotificationCenter", () => {
99
+ it("would throw an error if the component is not wrapped by a NotificationCenterContext.Provider", () => {
100
+ const FailingComponent = () => {
101
+ useNotificationCenter();
102
+ return <Box>FailingComponent</Box>;
103
+ };
104
+
105
+ try {
106
+ render(<FailingComponent />);
107
+ fail();
108
+ } catch (err) {
109
+ expect(err).toStrictEqual(NotificationCenterProviderUndefinedError);
110
+ }
111
+ });
112
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./tab-provider.context";
2
+ export * from "./tab-provider.provider";
@@ -0,0 +1,8 @@
1
+ import { createContext, Dispatch, SetStateAction, useContext } from "react";
2
+
3
+ export const TabContext = createContext<[number, Dispatch<SetStateAction<number>>]>([
4
+ 0,
5
+ () => null,
6
+ ]);
7
+ export const TabContextProvider = TabContext.Provider;
8
+ export const useTab = () => useContext(TabContext);
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { PropsWithChildren, useState } from "react";
3
+ import { TabContextProvider } from "./tab-provider.context";
4
+
5
+ type TabProviderProps = PropsWithChildren<{
6
+ initialValue?: number;
7
+ }>;
8
+
9
+ export const TabProvider = ({ children, initialValue = 0 }: TabProviderProps) => {
10
+ const tabState = useState(initialValue);
11
+
12
+ return <TabContextProvider value={tabState}>{children}</TabContextProvider>;
13
+ };
@@ -0,0 +1,90 @@
1
+ /* eslint-disable react/display-name */
2
+ import React, { FunctionComponent } from "react";
3
+ import { ComponentStory } from "@storybook/react";
4
+ import { JSXElementConstructor } from "react";
5
+ import { Box } from "@mui/material";
6
+ import { MemoryRouter, Router, Navigator, Route, Routes } from "react-router-dom";
7
+ import { NotificationCenterProvider } from "./providers";
8
+ import { action } from "@storybook/addon-actions";
9
+
10
+ export function createTemplate<P>(
11
+ C: JSXElementConstructor<P>,
12
+ ): ComponentStory<JSXElementConstructor<P>> {
13
+ return (args) => <C {...args} />;
14
+ }
15
+
16
+ const replaceAction = action("navigator/replace");
17
+ const goAction = action("navigator/go");
18
+ const pushAction = action("navigator/push");
19
+
20
+ export const withActionRouter =
21
+ ({ location = "/", path = "/" }: { location?: string; path?: string } = {}) =>
22
+ (Story: FunctionComponent) => {
23
+ const navigator: Navigator = {
24
+ replace: (args) => replaceAction(args),
25
+ go: (args) => goAction(args),
26
+ push: (args) => pushAction(args),
27
+ createHref: () => "",
28
+ };
29
+
30
+ return (
31
+ <Router location={location} navigator={navigator}>
32
+ <Routes>
33
+ <Route path={path} element={<Story />} />
34
+ </Routes>
35
+ </Router>
36
+ );
37
+ };
38
+
39
+ export const withMemoryRouter =
40
+ (initialEntries = ["/"]) =>
41
+ (Story: FunctionComponent) => {
42
+ return (
43
+ <MemoryRouter initialEntries={initialEntries}>
44
+ <Story />
45
+ </MemoryRouter>
46
+ );
47
+ };
48
+
49
+ export const withNotificationCenter = (Story: FunctionComponent) => {
50
+ return (
51
+ <NotificationCenterProvider>
52
+ <Story />
53
+ </NotificationCenterProvider>
54
+ );
55
+ };
56
+
57
+ export const withFullHeight = (Story: FunctionComponent) => {
58
+ return (
59
+ <Box height="100vh">
60
+ <Story />
61
+ </Box>
62
+ );
63
+ };
64
+
65
+ export const withContainer =
66
+ ({ width, height, bordered }: { width?: number; height?: number; bordered?: boolean }) =>
67
+ (Story: FunctionComponent) => {
68
+ let sx = {};
69
+ if (bordered) {
70
+ sx = {
71
+ ...sx,
72
+ border: "solid 1px black",
73
+ };
74
+ }
75
+ return (
76
+ <Box width={width} height={height} sx={sx}>
77
+ <Story />
78
+ </Box>
79
+ );
80
+ };
81
+
82
+ export const withPadding =
83
+ (padding = 2) =>
84
+ (Story: FunctionComponent) => {
85
+ return (
86
+ <Box padding={padding}>
87
+ <Story />
88
+ </Box>
89
+ );
90
+ };
@@ -0,0 +1,76 @@
1
+ import { AlertColor } from "@mui/material";
2
+ import { ModelField } from "../generators";
3
+ import { screen, waitForElementToBeRemoved } from "./testing-library";
4
+
5
+ export const expectContentPlaceholder = async () => {
6
+ expect(await screen.findByTestId(/content-placeholder-test/i)).toBeInTheDocument();
7
+ };
8
+
9
+ export const expectModelFieldInputExist = (fields: ModelField[]) => {
10
+ fields.forEach((field) => {
11
+ if (field.type === "group") {
12
+ expectModelFieldInputExist(field.value);
13
+ } else if (field.type === "number") {
14
+ expect(screen.getByRole("spinbutton", { name: field.name })).toBeInTheDocument();
15
+ } else {
16
+ expect(screen.getByRole("textbox", { name: field.name })).toBeInTheDocument();
17
+ }
18
+ });
19
+ };
20
+
21
+ export const expectModelFieldInputValue = (fields: ModelField[], initialValues: object) => {
22
+ fields.forEach((field) => {
23
+ const value = initialValues[field.id];
24
+ if (field.type === "group") {
25
+ expectModelFieldInputValue(field.value, value);
26
+ } else if (field.type === "number") {
27
+ expect(screen.getByDisplayValue(value.toString())).toBeInTheDocument();
28
+ } else {
29
+ expect(screen.getByDisplayValue(value.toString())).toBeInTheDocument();
30
+ }
31
+ });
32
+ };
33
+
34
+ export const expectModelFieldValue = (field: ModelField, instance: object) => {
35
+ const { id, type, name, description } = field;
36
+ const value = instance[id];
37
+
38
+ if (type === "group") {
39
+ expect(screen.getByRole("heading", { level: 1, name }));
40
+ expect(screen.getByRole("heading", { level: 2, name: description }));
41
+ field.value.forEach((groupValue) => expectModelFieldValue(groupValue, value));
42
+ return;
43
+ }
44
+
45
+ expect(screen.getByRole("label", { name: name })).toBeInTheDocument();
46
+ if (type === "boolean") {
47
+ expect(screen.getByTestId(value ? "CheckIcon" : "CloseIcon")).toBeInTheDocument();
48
+ } else {
49
+ expect(screen.getByLabelText(name)).toHaveTextContent(value);
50
+ //expect(screen.getByText(value)).toBeInTheDocument();
51
+ }
52
+ };
53
+
54
+ export const expectProgressIndicator = () => {
55
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
56
+ };
57
+
58
+ export const waitForProgressIndicatorToBeRemoved = async () => {
59
+ await waitForElementToBeRemoved(() => screen.getByRole("progressbar"));
60
+ };
61
+
62
+ export const expectAlert = async ({
63
+ title,
64
+ message,
65
+ severity,
66
+ }: {
67
+ title?: RegExp | string;
68
+ message: RegExp | string;
69
+ severity: AlertColor;
70
+ }) => {
71
+ const alertElement = await screen.findByRole("alert");
72
+ expect(alertElement).toBeInTheDocument();
73
+ expect(alertElement).toHaveAttribute("aria-label", severity);
74
+ title && expect(await screen.findByText(title)).toBeInTheDocument();
75
+ expect(await screen.findByText(message)).toBeInTheDocument();
76
+ };
@@ -0,0 +1,60 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Typography, Container, Skeleton, Grid, Box } from "@mui/material";
3
+ import { loremIpsum } from "lorem-ipsum";
4
+ import { newArrayWithSize } from "../utils";
5
+
6
+ export const Placeholder = () => {
7
+ return (
8
+ <Typography variant="body1">
9
+ {loremIpsum({
10
+ count: 3,
11
+ units: "paragraph",
12
+ })}
13
+ </Typography>
14
+ );
15
+ };
16
+
17
+ interface SkeletonCardProps {
18
+ width?: number | string;
19
+ animation?: "pulse" | "wave" | false;
20
+ }
21
+
22
+ export const SkeletonCard = ({ width = "100%", animation = false }: SkeletonCardProps) => {
23
+ return (
24
+ <Box width={width}>
25
+ <Skeleton animation={animation} variant="rectangular" height={118} />
26
+ <Skeleton animation={animation} variant="rectangular" height={16} sx={{ my: 1 }} />
27
+ <Skeleton animation={animation} variant="rectangular" width="80%" height={16} />
28
+ </Box>
29
+ );
30
+ };
31
+
32
+ interface SkeletonGridProps {
33
+ size?: number;
34
+ }
35
+
36
+ export const SkeletonGrid = ({ size = 20 }: SkeletonGridProps) => {
37
+ return (
38
+ <Grid container spacing={2}>
39
+ {newArrayWithSize(size, 0).map((_, i) => (
40
+ <Grid item key={i} xs={4}>
41
+ <SkeletonCard width={1} />
42
+ </Grid>
43
+ ))}
44
+ </Grid>
45
+ );
46
+ };
47
+
48
+ type ContentPlaceholderProps = PropsWithChildren<{
49
+ size?: number;
50
+ p?: number;
51
+ }>;
52
+
53
+ export const ContentPlaceholder = ({ size = 20, children, p }: ContentPlaceholderProps) => {
54
+ return (
55
+ <Container component="main" sx={{ p }} data-testid="content-placeholder-test">
56
+ {children}
57
+ <SkeletonGrid size={size} />
58
+ </Container>
59
+ );
60
+ };
@@ -0,0 +1,16 @@
1
+ import { ComponentMeta } from "@storybook/react";
2
+ import { createTemplate } from "../storybook";
3
+ import { ContentPlaceholder } from "./components";
4
+
5
+ export default {
6
+ title: "Test/ContentPlaceholder",
7
+ component: ContentPlaceholder,
8
+ parameters: {
9
+ layout: "fullscreen",
10
+ },
11
+ } as ComponentMeta<typeof ContentPlaceholder>;
12
+
13
+ const Template = createTemplate(ContentPlaceholder);
14
+
15
+ export const Default = Template.bind({});
16
+ Default.args = {};
@@ -0,0 +1,3 @@
1
+ export * from "./components";
2
+ export * from "./testing-library";
3
+ export * from "./assertions";
@@ -0,0 +1,18 @@
1
+ import { ComponentMeta } from "@storybook/react";
2
+ import { createTemplate } from "../storybook";
3
+ import { SkeletonCard } from "./components";
4
+
5
+ export default {
6
+ title: "Test/SkeletonCard",
7
+ component: SkeletonCard,
8
+ parameters: {
9
+ layout: "fullscreen",
10
+ },
11
+ } as ComponentMeta<typeof SkeletonCard>;
12
+
13
+ const Template = createTemplate(SkeletonCard);
14
+
15
+ export const Default = Template.bind({});
16
+ Default.args = {
17
+ width: 250,
18
+ };
@@ -0,0 +1,65 @@
1
+ import { render, RenderOptions } from "@testing-library/react";
2
+ import { MemoryRouter, Router } from "react-router-dom";
3
+ import { createMemoryHistory, MemoryHistory } from "history";
4
+ import React from "react";
5
+ import { ThemeProvider } from "@emotion/react";
6
+ import { Theme, createTheme, PaletteMode } from "@mui/material";
7
+
8
+ export type TestRouter = "router" | "memory";
9
+
10
+ function createMockTheme(mode: PaletteMode) {
11
+ return createTheme({
12
+ palette: {
13
+ mode,
14
+ },
15
+ });
16
+ }
17
+
18
+ function createMockHistory(): MemoryHistory {
19
+ return createMemoryHistory();
20
+ }
21
+
22
+ const createWrapper =
23
+ (history: MemoryHistory, theme: Theme, router: TestRouter) =>
24
+ // eslint-disable-next-line react/display-name
25
+ ({ children }: { children: React.ReactElement }) => {
26
+ const isMemoryRouter = router === "memory";
27
+
28
+ const R = isMemoryRouter ? MemoryRouter : Router;
29
+ const routerArgs = isMemoryRouter
30
+ ? {}
31
+ : {
32
+ location: history.location,
33
+ navigator: history,
34
+ };
35
+ return (
36
+ <ThemeProvider theme={theme}>
37
+ <R {...routerArgs}>{children}</R>
38
+ </ThemeProvider>
39
+ );
40
+ };
41
+
42
+ interface CustomRenderOptions {
43
+ renderOptions?: RenderOptions;
44
+ mode?: PaletteMode;
45
+ router?: TestRouter;
46
+ }
47
+
48
+ const customRender = (ui: React.ReactElement, options: CustomRenderOptions = {}) => {
49
+ const renderOptions = options.renderOptions || {};
50
+ const mode = options.mode || "light";
51
+ const router = options.router || "router";
52
+
53
+ const history = createMockHistory();
54
+ const theme = createMockTheme(mode);
55
+ const wrapper = createWrapper(history, theme, router);
56
+
57
+ const instance = render(ui, { wrapper, ...renderOptions });
58
+ return { ...instance, history };
59
+ };
60
+
61
+ // re-export everything
62
+ export * from "@testing-library/react";
63
+
64
+ // override render method
65
+ export { customRender as render };
@@ -0,0 +1,9 @@
1
+ import { newArrayWithSize } from "./arrays";
2
+
3
+ describe("newArrayWithSize", () => {
4
+ it("would create a new array with size 3 filled with 5", () => {
5
+ const a = newArrayWithSize(3, 5);
6
+ expect(a).toHaveLength(3);
7
+ expect(a.every((x) => x === 5)).toBeTruthy();
8
+ });
9
+ });
@@ -0,0 +1,7 @@
1
+ export const newArrayWithSize = <T>(size: number, fillValue: T) => new Array(size).fill(fillValue);
2
+
3
+ export const getRandomItem = <T>(items: T[]): { index: number; item: T } => {
4
+ const index = Math.floor(Math.random() * items.length);
5
+ const item = items[index];
6
+ return { index, item };
7
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./arrays";
2
+ export * from "./theme";