@arbor-education/design-system.components 0.1.4 → 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 (140) hide show
  1. package/.github/workflows/release.yml +17 -18
  2. package/CHANGELOG.md +12 -0
  3. package/dist/components/formField/fieldset/Fieldset.d.ts +6 -0
  4. package/dist/components/formField/fieldset/Fieldset.d.ts.map +1 -0
  5. package/dist/components/formField/fieldset/Fieldset.js +7 -0
  6. package/dist/components/formField/fieldset/Fieldset.js.map +1 -0
  7. package/dist/components/formField/fieldset/Fieldset.stories.d.ts +28 -0
  8. package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -0
  9. package/dist/components/formField/fieldset/Fieldset.stories.js +51 -0
  10. package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -0
  11. package/dist/components/formField/fieldset/Fieldset.test.d.ts +2 -0
  12. package/dist/components/formField/fieldset/Fieldset.test.d.ts.map +1 -0
  13. package/dist/components/formField/fieldset/Fieldset.test.js +62 -0
  14. package/dist/components/formField/fieldset/Fieldset.test.js.map +1 -0
  15. package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts +8 -0
  16. package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts.map +1 -0
  17. package/dist/components/formField/inputs/checkbox/CheckboxGroup.js +8 -0
  18. package/dist/components/formField/inputs/checkbox/CheckboxGroup.js.map +1 -0
  19. package/dist/components/formField/inputs/checkbox/CheckboxGroup.test.d.ts +2 -0
  20. package/dist/components/formField/inputs/checkbox/CheckboxGroup.test.d.ts.map +1 -0
  21. package/dist/components/formField/inputs/checkbox/CheckboxGroup.test.js +86 -0
  22. package/dist/components/formField/inputs/checkbox/CheckboxGroup.test.js.map +1 -0
  23. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +3 -1
  24. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  25. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +7 -0
  26. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  27. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts +11 -0
  28. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts.map +1 -0
  29. package/dist/components/formField/inputs/radio/RadioButtonGroup.js +8 -0
  30. package/dist/components/formField/inputs/radio/RadioButtonGroup.js.map +1 -0
  31. package/dist/components/formField/inputs/radio/RadioButtonGroup.test.d.ts +2 -0
  32. package/dist/components/formField/inputs/radio/RadioButtonGroup.test.d.ts.map +1 -0
  33. package/dist/components/formField/inputs/radio/RadioButtonGroup.test.js +86 -0
  34. package/dist/components/formField/inputs/radio/RadioButtonGroup.test.js.map +1 -0
  35. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  36. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +8 -2
  37. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  38. package/dist/components/icon/Icon.test.js +6 -3
  39. package/dist/components/icon/Icon.test.js.map +1 -1
  40. package/dist/components/separator/Separator.d.ts +7 -0
  41. package/dist/components/separator/Separator.d.ts.map +1 -0
  42. package/dist/components/separator/Separator.js +8 -0
  43. package/dist/components/separator/Separator.js.map +1 -0
  44. package/dist/components/separator/Separator.stories.d.ts +10 -0
  45. package/dist/components/separator/Separator.stories.d.ts.map +1 -0
  46. package/dist/components/separator/Separator.stories.js +12 -0
  47. package/dist/components/separator/Separator.stories.js.map +1 -0
  48. package/dist/components/separator/Separator.test.d.ts +2 -0
  49. package/dist/components/separator/Separator.test.d.ts.map +1 -0
  50. package/dist/components/separator/Separator.test.js +10 -0
  51. package/dist/components/separator/Separator.test.js.map +1 -0
  52. package/dist/components/table/Table.d.ts +13 -0
  53. package/dist/components/table/Table.d.ts.map +1 -1
  54. package/dist/components/table/Table.js +43 -7
  55. package/dist/components/table/Table.js.map +1 -1
  56. package/dist/components/table/Table.stories.d.ts.map +1 -1
  57. package/dist/components/table/Table.stories.js +8 -1
  58. package/dist/components/table/Table.stories.js.map +1 -1
  59. package/dist/components/table/Table.test.js +254 -2
  60. package/dist/components/table/Table.test.js.map +1 -1
  61. package/dist/components/table/pagination/TableSettingsDropdown.d.ts +2 -0
  62. package/dist/components/table/pagination/TableSettingsDropdown.d.ts.map +1 -0
  63. package/dist/components/table/pagination/TableSettingsDropdown.js +43 -0
  64. package/dist/components/table/pagination/TableSettingsDropdown.js.map +1 -0
  65. package/dist/components/table/useTableSettings.d.ts +22 -0
  66. package/dist/components/table/useTableSettings.d.ts.map +1 -0
  67. package/dist/components/table/useTableSettings.js +28 -0
  68. package/dist/components/table/useTableSettings.js.map +1 -0
  69. package/dist/index.css +31 -1
  70. package/dist/index.css.map +1 -1
  71. package/dist/index.d.ts +2 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +2 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/utils/hooks/useComponentDidMount.test.d.ts +2 -0
  76. package/dist/utils/hooks/useComponentDidMount.test.d.ts.map +1 -0
  77. package/dist/utils/hooks/useComponentDidMount.test.js +43 -0
  78. package/dist/utils/hooks/useComponentDidMount.test.js.map +1 -0
  79. package/dist/utils/hooks/useComponentDidUpdate.d.ts +7 -0
  80. package/dist/utils/hooks/useComponentDidUpdate.d.ts.map +1 -0
  81. package/dist/utils/hooks/useComponentDidUpdate.js +18 -0
  82. package/dist/utils/hooks/useComponentDidUpdate.js.map +1 -0
  83. package/dist/utils/hooks/useComponentDidUpdate.test.d.ts +2 -0
  84. package/dist/utils/hooks/useComponentDidUpdate.test.d.ts.map +1 -0
  85. package/dist/utils/hooks/useComponentDidUpdate.test.js +69 -0
  86. package/dist/utils/hooks/useComponentDidUpdate.test.js.map +1 -0
  87. package/dist/utils/hooks/useGridApi.test.d.ts +2 -0
  88. package/dist/utils/hooks/useGridApi.test.d.ts.map +1 -0
  89. package/dist/utils/hooks/useGridApi.test.js +48 -0
  90. package/dist/utils/hooks/useGridApi.test.js.map +1 -0
  91. package/dist/utils/hooks/useIsMounted.test.d.ts +2 -0
  92. package/dist/utils/hooks/useIsMounted.test.d.ts.map +1 -0
  93. package/dist/utils/hooks/useIsMounted.test.js +46 -0
  94. package/dist/utils/hooks/useIsMounted.test.js.map +1 -0
  95. package/dist/utils/hooks/useMemoGenerateUuid.d.ts +1 -1
  96. package/dist/utils/hooks/useMemoGenerateUuid.d.ts.map +1 -1
  97. package/dist/utils/hooks/useMemoGenerateUuid.js +1 -1
  98. package/dist/utils/hooks/useMemoGenerateUuid.js.map +1 -1
  99. package/dist/utils/hooks/useMemoGenerateUuid.test.d.ts +2 -0
  100. package/dist/utils/hooks/useMemoGenerateUuid.test.d.ts.map +1 -0
  101. package/dist/utils/hooks/useMemoGenerateUuid.test.js +55 -0
  102. package/dist/utils/hooks/useMemoGenerateUuid.test.js.map +1 -0
  103. package/dist/utils/hooks/usePubSub.test.d.ts +2 -0
  104. package/dist/utils/hooks/usePubSub.test.d.ts.map +1 -0
  105. package/dist/utils/hooks/usePubSub.test.js +81 -0
  106. package/dist/utils/hooks/usePubSub.test.js.map +1 -0
  107. package/package.json +2 -1
  108. package/src/components/formField/fieldset/Fieldset.stories.tsx +89 -0
  109. package/src/components/formField/fieldset/Fieldset.test.tsx +85 -0
  110. package/src/components/formField/fieldset/Fieldset.tsx +17 -0
  111. package/src/components/formField/fieldset/fieldset.scss +19 -0
  112. package/src/components/formField/inputs/checkbox/CheckboxGroup.test.tsx +127 -0
  113. package/src/components/formField/inputs/checkbox/CheckboxGroup.tsx +17 -0
  114. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +12 -1
  115. package/src/components/formField/inputs/radio/RadioButtonGroup.test.tsx +190 -0
  116. package/src/components/formField/inputs/radio/RadioButtonGroup.tsx +22 -0
  117. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +16 -7
  118. package/src/components/formField/label/label.scss +1 -1
  119. package/src/components/icon/Icon.test.tsx +6 -3
  120. package/src/components/separator/Separator.stories.tsx +15 -0
  121. package/src/components/separator/Separator.test.tsx +10 -0
  122. package/src/components/separator/Separator.tsx +15 -0
  123. package/src/components/separator/separator.scss +6 -0
  124. package/src/components/table/Table.stories.tsx +8 -1
  125. package/src/components/table/Table.test.tsx +444 -1
  126. package/src/components/table/Table.tsx +69 -24
  127. package/src/components/table/pagination/TableSettingsDropdown.tsx +90 -0
  128. package/src/components/table/table.scss +6 -0
  129. package/src/components/table/useTableSettings.ts +47 -0
  130. package/src/index.scss +2 -0
  131. package/src/index.ts +2 -0
  132. package/src/utils/hooks/useComponentDidMount.test.tsx +59 -0
  133. package/src/utils/hooks/useComponentDidUpdate.test.ts +107 -0
  134. package/src/utils/hooks/useComponentDidUpdate.ts +19 -0
  135. package/src/utils/hooks/useGridApi.test.tsx +79 -0
  136. package/src/utils/hooks/useIsMounted.test.tsx +63 -0
  137. package/src/utils/hooks/useMemoGenerateUuid.test.tsx +80 -0
  138. package/src/utils/hooks/useMemoGenerateUuid.ts +1 -1
  139. package/src/utils/hooks/usePubSub.test.tsx +100 -0
  140. package/vitest.config.ts +9 -0
@@ -0,0 +1,47 @@
1
+ import { useState } from 'react';
2
+ import { TABLE_SPACING } from './Table';
3
+ import { useComponentDidUpdate } from 'Utils/hooks/useComponentDidUpdate';
4
+
5
+ export type TableSettings = {
6
+ hasColumnBorders?: boolean;
7
+ setHasColumnBorders: (val: boolean) => void;
8
+ tableSpacing: TABLE_SPACING;
9
+ setTableSpacing: (val: TABLE_SPACING) => void;
10
+ };
11
+
12
+ type UseTableSettingsParams = {
13
+ onTableSettingsChanged?: (val: TableSettings) => void;
14
+ onTableSettingsReset?: () => void;
15
+ };
16
+
17
+ export const useTableSettings = (params: UseTableSettingsParams = {}) => {
18
+ const {
19
+ onTableSettingsChanged,
20
+ onTableSettingsReset,
21
+ } = params;
22
+ const [hasColumnBorders, setHasColumnBorders] = useState(false);
23
+ const [tableSpacing, setTableSpacing] = useState(TABLE_SPACING.M);
24
+
25
+ const settings = {
26
+ hasColumnBorders,
27
+ setHasColumnBorders,
28
+ tableSpacing,
29
+ setTableSpacing,
30
+ };
31
+
32
+ const resetSettings = () => {
33
+ setHasColumnBorders(false);
34
+ setTableSpacing(TABLE_SPACING.M);
35
+ if (onTableSettingsReset) {
36
+ onTableSettingsReset();
37
+ }
38
+ };
39
+
40
+ useComponentDidUpdate(() => {
41
+ if (onTableSettingsChanged) {
42
+ onTableSettingsChanged(settings);
43
+ }
44
+ }, [hasColumnBorders, tableSpacing]);
45
+
46
+ return { settings, resetSettings };
47
+ };
package/src/index.scss CHANGED
@@ -6,6 +6,7 @@
6
6
  @use "components/card/card.scss";
7
7
  @use "components/dropdown/dropdown.scss";
8
8
  @use "components/formField/formField.scss";
9
+ @use "components/formField/fieldset/fieldset.scss";
9
10
  @use "components/formField/inputs/input.scss";
10
11
  @use "components/formField/label/label.scss";
11
12
  @use "components/formField/inputs/number/numberInput.scss";
@@ -22,4 +23,5 @@
22
23
  @use "components/searchBar/searchBar.scss";
23
24
  @use "components/table/pagination/pagination.scss";
24
25
  @use "components/tooltip/tooltip.scss";
26
+ @use "components/separator/separator.scss";
25
27
  @import "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap";
package/src/index.ts CHANGED
@@ -19,3 +19,5 @@ export { Table } from 'Components/table/Table';
19
19
  export { GridApiContext } from 'Components/table/GridApiContext';
20
20
  export { Tooltip } from 'Components/tooltip/Tooltip';
21
21
  export { TooltipWrapper } from 'Components/tooltip/TooltipWrapper';
22
+ export { Fieldset } from 'Components/formField/fieldset/Fieldset';
23
+ export { Separator } from 'Components/separator/Separator';
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import { useComponentDidMount } from './useComponentDidMount';
4
+
5
+ describe('useComponentDidMount', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+
10
+ test('calls callback on mount', () => {
11
+ const callback = vi.fn();
12
+
13
+ const TestComponent = () => {
14
+ useComponentDidMount(callback);
15
+ return <div>Test</div>;
16
+ };
17
+
18
+ render(<TestComponent />);
19
+
20
+ expect(callback).toHaveBeenCalledTimes(1);
21
+ });
22
+
23
+ test('does not call callback on re-render', () => {
24
+ const callback = vi.fn();
25
+
26
+ const TestComponent = ({ count }: { count: number }) => {
27
+ useComponentDidMount(callback);
28
+ return (
29
+ <div>
30
+ Test
31
+ {count}
32
+ </div>
33
+ );
34
+ };
35
+
36
+ const { rerender } = render(<TestComponent count={1} />);
37
+ expect(callback).toHaveBeenCalledTimes(1);
38
+
39
+ rerender(<TestComponent count={2} />);
40
+ expect(callback).toHaveBeenCalledTimes(1);
41
+ });
42
+
43
+ test('calls cleanup function on unmount', () => {
44
+ const cleanup = vi.fn();
45
+ const callback = vi.fn(() => cleanup);
46
+
47
+ const TestComponent = () => {
48
+ useComponentDidMount(callback);
49
+ return <div>Test</div>;
50
+ };
51
+
52
+ const { unmount } = render(<TestComponent />);
53
+ expect(callback).toHaveBeenCalledTimes(1);
54
+ expect(cleanup).not.toHaveBeenCalled();
55
+
56
+ unmount();
57
+ expect(cleanup).toHaveBeenCalledTimes(1);
58
+ });
59
+ });
@@ -0,0 +1,107 @@
1
+ import { expect, test, describe, vi } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useComponentDidUpdate } from './useComponentDidUpdate';
4
+
5
+ describe('useComponentDidUpdate', () => {
6
+ test('does not call effect on initial render', () => {
7
+ const effect = vi.fn();
8
+ renderHook(() => useComponentDidUpdate(effect, []));
9
+
10
+ expect(effect).not.toHaveBeenCalled();
11
+ });
12
+
13
+ test('calls effect on subsequent renders when dependencies change', () => {
14
+ const effect = vi.fn();
15
+ const { rerender } = renderHook(
16
+ ({ deps }) => useComponentDidUpdate(effect, deps),
17
+ { initialProps: { deps: [1] } },
18
+ );
19
+
20
+ expect(effect).not.toHaveBeenCalled();
21
+
22
+ rerender({ deps: [2] });
23
+ expect(effect).toHaveBeenCalledTimes(1);
24
+ });
25
+
26
+ test('does not call effect when dependencies do not change', () => {
27
+ const effect = vi.fn();
28
+ const { rerender } = renderHook(
29
+ ({ deps }) => useComponentDidUpdate(effect, deps),
30
+ { initialProps: { deps: [1] } },
31
+ );
32
+
33
+ expect(effect).not.toHaveBeenCalled();
34
+
35
+ rerender({ deps: [1] });
36
+ expect(effect).not.toHaveBeenCalled();
37
+ });
38
+
39
+ test('calls effect multiple times on multiple updates', () => {
40
+ const effect = vi.fn();
41
+ const { rerender } = renderHook(
42
+ ({ deps }) => useComponentDidUpdate(effect, deps),
43
+ { initialProps: { deps: [1] } },
44
+ );
45
+
46
+ rerender({ deps: [2] });
47
+ expect(effect).toHaveBeenCalledTimes(1);
48
+
49
+ rerender({ deps: [3] });
50
+ expect(effect).toHaveBeenCalledTimes(2);
51
+
52
+ rerender({ deps: [4] });
53
+ expect(effect).toHaveBeenCalledTimes(3);
54
+ });
55
+
56
+ test('calls cleanup function returned from effect', () => {
57
+ const cleanup = vi.fn();
58
+ const effect = vi.fn(() => cleanup);
59
+ const { rerender, unmount } = renderHook(
60
+ ({ deps }) => useComponentDidUpdate(effect, deps),
61
+ { initialProps: { deps: [1] } },
62
+ );
63
+
64
+ rerender({ deps: [2] });
65
+ expect(effect).toHaveBeenCalledTimes(1);
66
+ expect(cleanup).not.toHaveBeenCalled();
67
+
68
+ rerender({ deps: [3] });
69
+ expect(cleanup).toHaveBeenCalledTimes(1);
70
+ expect(effect).toHaveBeenCalledTimes(2);
71
+
72
+ unmount();
73
+ expect(cleanup).toHaveBeenCalledTimes(2);
74
+ });
75
+
76
+ test('works with multiple dependencies', () => {
77
+ const effect = vi.fn();
78
+ const { rerender } = renderHook(
79
+ ({ deps }) => useComponentDidUpdate(effect, deps),
80
+ { initialProps: { deps: [1, 'a', true] } },
81
+ );
82
+
83
+ expect(effect).not.toHaveBeenCalled();
84
+
85
+ // Change one dependency
86
+ rerender({ deps: [1, 'a', false] });
87
+ expect(effect).toHaveBeenCalledTimes(1);
88
+
89
+ // Change another dependency
90
+ rerender({ deps: [1, 'b', false] });
91
+ expect(effect).toHaveBeenCalledTimes(2);
92
+
93
+ // Change multiple dependencies
94
+ rerender({ deps: [2, 'c', true] });
95
+ expect(effect).toHaveBeenCalledTimes(3);
96
+ });
97
+
98
+ test('works with empty dependency array', () => {
99
+ const effect = vi.fn();
100
+ const { rerender } = renderHook(() => useComponentDidUpdate(effect, []));
101
+
102
+ expect(effect).not.toHaveBeenCalled();
103
+
104
+ rerender();
105
+ expect(effect).not.toHaveBeenCalled();
106
+ });
107
+ });
@@ -0,0 +1,19 @@
1
+ import { useEffect, useRef, type DependencyList, type EffectCallback } from 'react';
2
+
3
+ /**
4
+ * This is a hook that is functionally identical to useEffect, except it only runs on
5
+ * updates, never on first render
6
+ */
7
+ export const useComponentDidUpdate = (effect: EffectCallback, deps: DependencyList) => {
8
+ const isFirstRenderRef = useRef(true);
9
+
10
+ useEffect(() => {
11
+ if (isFirstRenderRef.current) {
12
+ isFirstRenderRef.current = false;
13
+ return;
14
+ }
15
+ else {
16
+ return effect();
17
+ }
18
+ }, deps);
19
+ };
@@ -0,0 +1,79 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import { useGridApi } from './useGridApi';
4
+ import { GridApiContext } from 'Components/table/GridApiContext';
5
+ import type { GridApi } from 'ag-grid-community';
6
+
7
+ describe('useGridApi', () => {
8
+ test('returns null gridApi and false isGridApiReady when context is null', () => {
9
+ let result: { gridApi: GridApi | null; isGridApiReady: boolean } = { gridApi: null, isGridApiReady: false };
10
+
11
+ const TestComponent = () => {
12
+ result = useGridApi();
13
+ return <div>Test</div>;
14
+ };
15
+
16
+ render(
17
+ <GridApiContext.Provider value={null}>
18
+ <TestComponent />
19
+ </GridApiContext.Provider>,
20
+ );
21
+
22
+ expect(result).not.toBeNull();
23
+ expect(result?.gridApi).toBeNull();
24
+ expect(result?.isGridApiReady).toBe(false);
25
+ });
26
+
27
+ test('returns gridApi and true isGridApiReady when context provides gridApi', () => {
28
+ const mockGridApi = {
29
+ getDisplayedRowCount: vi.fn(),
30
+ } as unknown as GridApi;
31
+
32
+ let result: { gridApi: GridApi | null; isGridApiReady: boolean } = { gridApi: null, isGridApiReady: false };
33
+
34
+ const TestComponent = () => {
35
+ result = useGridApi();
36
+ return <div>Test</div>;
37
+ };
38
+
39
+ render(
40
+ <GridApiContext.Provider value={mockGridApi}>
41
+ <TestComponent />
42
+ </GridApiContext.Provider>,
43
+ );
44
+
45
+ expect(result).not.toBeNull();
46
+ expect(result?.gridApi).toBe(mockGridApi);
47
+ expect(result?.isGridApiReady).toBe(true);
48
+ });
49
+
50
+ test('updates isGridApiReady when gridApi changes from null to provided', () => {
51
+ const mockGridApi = {
52
+ getDisplayedRowCount: vi.fn(),
53
+ } as unknown as GridApi;
54
+
55
+ let result: { gridApi: GridApi | null; isGridApiReady: boolean } = { gridApi: null, isGridApiReady: false };
56
+
57
+ const TestComponent = () => {
58
+ result = useGridApi();
59
+ return <div>Test</div>;
60
+ };
61
+
62
+ const { rerender } = render(
63
+ <GridApiContext.Provider value={null}>
64
+ <TestComponent />
65
+ </GridApiContext.Provider>,
66
+ );
67
+
68
+ expect(result?.isGridApiReady).toBe(false);
69
+
70
+ rerender(
71
+ <GridApiContext.Provider value={mockGridApi}>
72
+ <TestComponent />
73
+ </GridApiContext.Provider>,
74
+ );
75
+
76
+ expect(result?.isGridApiReady).toBe(true);
77
+ expect(result?.gridApi).toBe(mockGridApi);
78
+ });
79
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import { useIsMounted } from './useIsMounted';
4
+
5
+ describe('useIsMounted', () => {
6
+ test('returns a ref with current value true when component is mounted', () => {
7
+ let mountedRef: ReturnType<typeof useIsMounted> | null = null;
8
+
9
+ const TestComponent = () => {
10
+ mountedRef = useIsMounted();
11
+ return <div>Test</div>;
12
+ };
13
+
14
+ render(<TestComponent />);
15
+
16
+ expect(mountedRef).not.toBeNull();
17
+ expect(mountedRef!.current).toBe(true);
18
+ });
19
+
20
+ test('returns the same ref instance across re-renders', () => {
21
+ let mountedRef1: ReturnType<typeof useIsMounted> | null = null;
22
+ let mountedRef2: ReturnType<typeof useIsMounted> | null = null;
23
+
24
+ const TestComponent = ({ count }: { count: number }) => {
25
+ const ref = useIsMounted();
26
+ if (count === 1) {
27
+ mountedRef1 = ref;
28
+ }
29
+ else {
30
+ mountedRef2 = ref;
31
+ }
32
+ return (
33
+ <div>
34
+ Test
35
+ {count}
36
+ </div>
37
+ );
38
+ };
39
+
40
+ const { rerender } = render(<TestComponent count={1} />);
41
+ rerender(<TestComponent count={2} />);
42
+
43
+ expect(mountedRef1).toBe(mountedRef2);
44
+ expect(mountedRef1!.current).toBe(true);
45
+ });
46
+
47
+ test('sets current to false when component unmounts', () => {
48
+ let mountedRef: ReturnType<typeof useIsMounted> | null = null;
49
+
50
+ const TestComponent = () => {
51
+ mountedRef = useIsMounted();
52
+ return <div>Test</div>;
53
+ };
54
+
55
+ const { unmount } = render(<TestComponent />);
56
+
57
+ expect(mountedRef!.current).toBe(true);
58
+
59
+ unmount();
60
+
61
+ expect(mountedRef!.current).toBe(false);
62
+ });
63
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import { useMemoGenerateUuid } from './useMemoGenerateUuid';
4
+ import { generateUuid } from '../generateUuid';
5
+
6
+ vi.mock('../generateUuid', () => ({
7
+ generateUuid: vi.fn((prefix?: string) => {
8
+ const uuid = '123e4567-e89b-12d3-a456-426614174000';
9
+ return prefix ? `${prefix}-${uuid}` : uuid;
10
+ }),
11
+ }));
12
+
13
+ describe('useMemoGenerateUuid', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ test('generates a UUID without prefix', () => {
19
+ let uuid: string | null = null;
20
+
21
+ const TestComponent = () => {
22
+ uuid = useMemoGenerateUuid();
23
+ return <div>Test</div>;
24
+ };
25
+
26
+ render(<TestComponent />);
27
+
28
+ expect(uuid).toBe('123e4567-e89b-12d3-a456-426614174000');
29
+ expect(generateUuid).toHaveBeenCalledWith(undefined);
30
+ });
31
+
32
+ test('generates a UUID with prefix', () => {
33
+ let uuid: string | null = null;
34
+
35
+ const TestComponent = () => {
36
+ uuid = useMemoGenerateUuid({ prefix: 'test' });
37
+ return <div>Test</div>;
38
+ };
39
+
40
+ render(<TestComponent />);
41
+
42
+ expect(uuid).toBe('test-123e4567-e89b-12d3-a456-426614174000');
43
+ expect(generateUuid).toHaveBeenCalledWith('test');
44
+ });
45
+
46
+ test('does not generate a new UUID when the dependencies do not change', () => {
47
+ const TestComponent = ({ count }: { count: number }) => {
48
+ const uuid = useMemoGenerateUuid();
49
+ return (
50
+ <div>
51
+ Test
52
+ {count}
53
+ {uuid}
54
+ </div>
55
+ );
56
+ };
57
+
58
+ const { rerender } = render(<TestComponent count={1} />);
59
+ rerender(<TestComponent count={2} />);
60
+ expect(generateUuid).toHaveBeenCalledTimes(1);
61
+ });
62
+
63
+ test('generates a new UUID when the dependencies change', () => {
64
+ const TestComponent = ({ dep1, dep2 }: { dep1: number; dep2: string }) => {
65
+ const uuid = useMemoGenerateUuid({ deps: [dep1, dep2] });
66
+
67
+ return (
68
+ <div>
69
+ Test
70
+ {uuid}
71
+ </div>
72
+ );
73
+ };
74
+
75
+ const { rerender } = render(<TestComponent dep1={1} dep2="a" />);
76
+ rerender(<TestComponent dep1={2} dep2="b" />);
77
+
78
+ expect(generateUuid).toHaveBeenCalledTimes(2);
79
+ });
80
+ });
@@ -6,7 +6,7 @@ type Args = {
6
6
  prefix?: string;
7
7
  };
8
8
 
9
- export const useMemoGenerateUuid = ({ deps = [], prefix }: Args) => {
9
+ export const useMemoGenerateUuid = ({ deps = [], prefix }: Args = {}) => {
10
10
  return useMemo(() => {
11
11
  return generateUuid(prefix);
12
12
  }, deps);
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test, vi, afterEach } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import { usePubSub } from './usePubSub';
4
+ import PubSub from 'Utils/PubSub';
5
+ import { generateUuid } from 'Utils/generateUuid';
6
+
7
+ const mockSubscriptions: Record<string, Record<string, (...args: unknown[]) => void>> = {};
8
+ vi.mock('Utils/PubSub', () => {
9
+ return {
10
+ default: {
11
+ subscribe: vi.fn((eventName: string, handler: (...args: unknown[]) => void) => {
12
+ const subscriptionId = generateUuid('pubsub-subscription');
13
+ if (!mockSubscriptions[eventName]) {
14
+ mockSubscriptions[eventName] = {};
15
+ }
16
+ mockSubscriptions[eventName][subscriptionId] = handler;
17
+ return subscriptionId;
18
+ }),
19
+ unsubscribe: vi.fn((eventName: string, subscriptionId: string) => {
20
+ if (mockSubscriptions[eventName]) {
21
+ delete mockSubscriptions[eventName][subscriptionId];
22
+ }
23
+ }),
24
+ publish: vi.fn((eventName: string, data?: unknown) => {
25
+ if (mockSubscriptions[eventName]) {
26
+ Object.values(mockSubscriptions[eventName]).forEach((handler) => {
27
+ handler(data);
28
+ });
29
+ }
30
+ }),
31
+ // Helper to clear subscriptions for testing
32
+ _clearSubscriptions: () => {
33
+ Object.keys(mockSubscriptions).forEach((key) => {
34
+ delete mockSubscriptions[key];
35
+ });
36
+ },
37
+ },
38
+ };
39
+ });
40
+
41
+ describe('usePubSub', () => {
42
+ afterEach(() => {
43
+ // @ts-expect-error - Accessing private method for testing
44
+ PubSub._clearSubscriptions?.();
45
+ vi.clearAllMocks();
46
+ vi.resetAllMocks();
47
+ vi.restoreAllMocks();
48
+ });
49
+
50
+ test('subscribes to event on mount', () => {
51
+ const handler = vi.fn();
52
+
53
+ const TestComponent = () => {
54
+ usePubSub('test-event', handler);
55
+ return <div>Test</div>;
56
+ };
57
+
58
+ render(<TestComponent />);
59
+
60
+ expect(PubSub.subscribe).toHaveBeenCalledTimes(1);
61
+ expect(PubSub.subscribe).toHaveBeenCalledWith('test-event', handler);
62
+ });
63
+
64
+ test('unsubscribes from event on unmount', () => {
65
+ const handler = vi.fn();
66
+
67
+ const TestComponent = () => {
68
+ usePubSub('test-event', handler);
69
+ return <div>Test</div>;
70
+ };
71
+
72
+ const { unmount } = render(<TestComponent />);
73
+
74
+ expect(PubSub.subscribe).toHaveBeenCalledTimes(1);
75
+
76
+ unmount();
77
+
78
+ expect(PubSub.unsubscribe).toHaveBeenCalledWith('test-event', expect.any(String));
79
+ });
80
+
81
+ test('does not resubscribe on re-render', () => {
82
+ const handler = vi.fn();
83
+
84
+ const TestComponent = ({ count }: { count: number }) => {
85
+ usePubSub('test-event', handler);
86
+ return (
87
+ <div>
88
+ Test
89
+ {count}
90
+ </div>
91
+ );
92
+ };
93
+
94
+ const { rerender } = render(<TestComponent count={1} />);
95
+ expect(PubSub.subscribe).toHaveBeenCalledTimes(1);
96
+
97
+ rerender(<TestComponent count={2} />);
98
+ expect(PubSub.subscribe).toHaveBeenCalledTimes(1);
99
+ });
100
+ });
package/vitest.config.ts CHANGED
@@ -8,6 +8,15 @@ export default defineConfig({
8
8
  environment: 'jsdom',
9
9
  globals: true,
10
10
  setupFiles: ['./setupTestRuntime.ts'],
11
+ coverage: {
12
+ reporter: ['lcov'],
13
+ include: ['src/**/*.{ts,tsx}'],
14
+ exclude: [
15
+ 'src/**/*.mdx',
16
+ 'src/**/*.{stories,story,test}.{ts,tsx}',
17
+ 'src/**/types.ts',
18
+ ],
19
+ },
11
20
  },
12
21
  resolve: {
13
22
  alias: {