@cccsaurora/howler-ui 2.18.0-dev.841 → 2.19.0-cases.1040

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 (171) hide show
  1. package/api/action/index.d.ts +5 -4
  2. package/api/action/index.js +8 -8
  3. package/api/analytic/index.d.ts +3 -2
  4. package/api/analytic/index.js +4 -4
  5. package/api/analytic/owner.d.ts +2 -1
  6. package/api/analytic/owner.js +2 -2
  7. package/api/analytic/rules.d.ts +2 -1
  8. package/api/analytic/rules.js +2 -2
  9. package/api/dossier/index.d.ts +4 -3
  10. package/api/dossier/index.js +6 -6
  11. package/api/hit/index.d.ts +3 -2
  12. package/api/hit/index.js +4 -4
  13. package/api/hit/labels.d.ts +3 -2
  14. package/api/hit/labels.js +4 -4
  15. package/api/hit/overwrite.d.ts +2 -1
  16. package/api/hit/overwrite.js +2 -2
  17. package/api/hit/transition.d.ts +2 -1
  18. package/api/hit/transition.js +2 -2
  19. package/api/index.d.ts +5 -4
  20. package/api/index.js +8 -8
  21. package/api/overview/index.d.ts +4 -3
  22. package/api/overview/index.js +6 -6
  23. package/api/search/overview.d.ts +2 -2
  24. package/api/template/index.d.ts +4 -3
  25. package/api/template/index.js +6 -6
  26. package/api/user/index.d.ts +2 -1
  27. package/api/user/index.js +2 -2
  28. package/api/view/index.d.ts +4 -3
  29. package/api/view/index.js +6 -6
  30. package/components/app/App.d.ts +1 -0
  31. package/components/app/App.js +24 -6
  32. package/components/app/providers/FavouritesProvider.js +0 -4
  33. package/components/app/providers/ModalProvider.d.ts +1 -0
  34. package/components/app/providers/ModalProvider.js +5 -1
  35. package/components/app/providers/RecordProvider.d.ts +1 -1
  36. package/components/app/providers/RecordProvider.js +1 -1
  37. package/components/app/providers/SearchResponseProvider.d.ts +21 -0
  38. package/components/app/providers/SearchResponseProvider.js +81 -0
  39. package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
  40. package/components/app/providers/SearchResponseProvider.test.js +406 -0
  41. package/components/app/providers/ViewProvider.test.js +4 -1
  42. package/components/elements/addons/buttons/CustomButton.test.d.ts +1 -0
  43. package/components/elements/addons/buttons/CustomButton.test.js +106 -0
  44. package/components/elements/addons/layout/FlexOne.test.d.ts +1 -0
  45. package/components/elements/addons/layout/FlexOne.test.js +30 -0
  46. package/components/elements/addons/lists/TuiListBase.js +4 -0
  47. package/components/elements/addons/lists/TuiListElement.js +6 -3
  48. package/components/elements/addons/lists/index.d.ts +1 -0
  49. package/components/elements/addons/search/SearchPagination.d.ts +2 -1
  50. package/components/elements/addons/search/SearchPagination.js +4 -4
  51. package/components/elements/addons/search/SearchPagination.test.d.ts +1 -0
  52. package/components/elements/addons/search/SearchPagination.test.js +80 -0
  53. package/components/elements/addons/search/SearchTotal.test.d.ts +1 -0
  54. package/components/elements/addons/search/SearchTotal.test.js +47 -0
  55. package/components/elements/display/DocumentationButton.test.d.ts +1 -0
  56. package/components/elements/display/DocumentationButton.test.js +84 -0
  57. package/components/elements/display/DynamicTabs.test.d.ts +1 -0
  58. package/components/elements/display/DynamicTabs.test.js +93 -0
  59. package/components/elements/display/HowlerAvatar.js +2 -1
  60. package/components/elements/display/HowlerCard.test.d.ts +1 -0
  61. package/components/elements/display/HowlerCard.test.js +51 -0
  62. package/components/elements/display/Image.test.d.ts +1 -0
  63. package/components/elements/display/Image.test.js +79 -0
  64. package/components/elements/display/ItemManager.d.ts +2 -2
  65. package/components/elements/display/ItemManager.js +1 -1
  66. package/components/elements/display/Markdown.js +9 -2
  67. package/components/elements/display/QueryResultText.test.d.ts +1 -0
  68. package/components/elements/display/QueryResultText.test.js +48 -0
  69. package/components/elements/display/TextDivider.test.d.ts +1 -0
  70. package/components/elements/display/TextDivider.test.js +43 -0
  71. package/components/elements/display/TypingIndicator.test.d.ts +1 -0
  72. package/components/elements/display/TypingIndicator.test.js +33 -0
  73. package/components/elements/display/modals/ConfirmDeleteModal.d.ts +4 -0
  74. package/components/elements/display/modals/ConfirmDeleteModal.js +4 -2
  75. package/components/elements/hit/HitBanner.js +6 -3
  76. package/components/elements/hit/HitLinks.js +1 -1
  77. package/components/elements/hit/HitSummary.js +3 -2
  78. package/components/elements/hit/elements/AnalyticLink.js +1 -1
  79. package/components/elements/hit/elements/Assigned.d.ts +2 -0
  80. package/components/elements/hit/elements/Assigned.js +15 -13
  81. package/components/elements/record/RecordContextMenu.js +15 -3
  82. package/components/elements/record/RecordContextMenu.test.js +96 -0
  83. package/components/hooks/useMyApi.d.ts +1 -1
  84. package/components/hooks/useMyPreferences.js +2 -1
  85. package/components/routes/403.d.ts +3 -0
  86. package/components/routes/403.js +10 -0
  87. package/components/routes/403.test.d.ts +1 -0
  88. package/components/routes/403.test.js +27 -0
  89. package/components/routes/404.test.d.ts +1 -0
  90. package/components/routes/404.test.js +26 -0
  91. package/components/routes/ErrorBoundary.test.d.ts +1 -0
  92. package/components/routes/ErrorBoundary.test.js +43 -0
  93. package/components/routes/ErrorOccured.test.d.ts +1 -0
  94. package/components/routes/ErrorOccured.test.js +35 -0
  95. package/components/routes/Logout.test.d.ts +1 -0
  96. package/components/routes/Logout.test.js +59 -0
  97. package/components/routes/action/edit/ActionEditor.js +1 -1
  98. package/components/routes/action/useMyActionFunctions.js +30 -6
  99. package/components/routes/action/view/ActionDetails.js +15 -2
  100. package/components/routes/action/view/ActionSearch.js +32 -20
  101. package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
  102. package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
  103. package/components/routes/analytics/AnalyticDetails.js +10 -6
  104. package/components/routes/analytics/AnalyticSearch.js +6 -6
  105. package/components/routes/cases/Cases.js +5 -6
  106. package/components/routes/cases/modals/ResolveModal.js +14 -13
  107. package/components/routes/cases/modals/ResolveModal.test.js +85 -25
  108. package/components/routes/dossiers/Dossiers.js +25 -19
  109. package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
  110. package/components/routes/help/markdown/en/client.md.js +1 -1
  111. package/components/routes/help/markdown/en/retention.md.js +1 -1
  112. package/components/routes/help/markdown/fr/client.md.js +1 -1
  113. package/components/routes/help/markdown/fr/retention.md.js +1 -1
  114. package/components/routes/hits/search/shared/CustomSpan.js +7 -7
  115. package/components/routes/hits/search/shared/CustomSpan.test.d.ts +1 -0
  116. package/components/routes/hits/search/shared/CustomSpan.test.js +170 -0
  117. package/components/routes/home/AddNewCard.js +1 -1
  118. package/components/routes/home/AddNewCard.test.d.ts +1 -0
  119. package/components/routes/home/AddNewCard.test.js +86 -0
  120. package/components/routes/home/ViewCard.js +1 -1
  121. package/components/routes/home/ViewRefresh.test.d.ts +1 -0
  122. package/components/routes/home/ViewRefresh.test.js +79 -0
  123. package/components/routes/overviews/OverviewCard.test.d.ts +1 -0
  124. package/components/routes/overviews/OverviewCard.test.js +108 -0
  125. package/components/routes/overviews/OverviewViewer.js +16 -8
  126. package/components/routes/overviews/Overviews.js +25 -19
  127. package/components/routes/overviews/template/en.md.js +1 -1
  128. package/components/routes/overviews/template/fr.md.js +1 -1
  129. package/components/routes/settings/LocalSection.js +2 -1
  130. package/components/routes/settings/SettingsSection.test.d.ts +1 -0
  131. package/components/routes/settings/SettingsSection.test.js +19 -0
  132. package/components/routes/templates/TemplateCard.d.ts +3 -1
  133. package/components/routes/templates/TemplateCard.js +8 -4
  134. package/components/routes/templates/TemplateCard.test.d.ts +1 -0
  135. package/components/routes/templates/TemplateCard.test.js +115 -0
  136. package/components/routes/templates/TemplateViewer.js +62 -16
  137. package/components/routes/templates/Templates.js +35 -13
  138. package/components/routes/views/Views.js +21 -14
  139. package/locales/en/translation.json +25 -4
  140. package/locales/fr/translation.json +25 -4
  141. package/models/entities/generated/ApiType.d.ts +1 -1
  142. package/models/entities/generated/Hit.d.ts +1 -0
  143. package/models/entities/generated/Howler.d.ts +1 -0
  144. package/models/entities/generated/UserUser.d.ts +2 -0
  145. package/package.json +143 -148
  146. package/rest/FetchClient.test.d.ts +1 -0
  147. package/rest/FetchClient.test.js +81 -0
  148. package/setupTests.js +5 -0
  149. package/utils/Throttler.test.d.ts +1 -0
  150. package/utils/Throttler.test.js +135 -0
  151. package/utils/actionUtils.test.d.ts +1 -0
  152. package/utils/actionUtils.test.js +140 -0
  153. package/utils/constants.d.ts +1 -0
  154. package/utils/constants.js +1 -0
  155. package/utils/hitFunctions.test.d.ts +1 -0
  156. package/utils/hitFunctions.test.js +52 -0
  157. package/utils/localStorage.test.d.ts +1 -0
  158. package/utils/localStorage.test.js +96 -0
  159. package/utils/menuUtils.js +1 -1
  160. package/utils/menuUtils.test.d.ts +1 -0
  161. package/utils/menuUtils.test.js +198 -0
  162. package/utils/sessionStorage.test.d.ts +1 -0
  163. package/utils/sessionStorage.test.js +81 -0
  164. package/utils/stringUtils.test.d.ts +1 -0
  165. package/utils/stringUtils.test.js +159 -0
  166. package/utils/utils.test.d.ts +1 -0
  167. package/utils/utils.test.js +292 -0
  168. package/utils/viewUtils.test.d.ts +1 -0
  169. package/utils/viewUtils.test.js +45 -0
  170. package/utils/xsrf.test.d.ts +1 -0
  171. package/utils/xsrf.test.js +41 -0
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import { I18nextProvider } from 'react-i18next';
6
+ import TextDivider from './TextDivider';
7
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
8
+ describe('TextDivider', () => {
9
+ describe('rendering', () => {
10
+ it('renders without crashing', () => {
11
+ const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
12
+ expect(container.firstChild).toBeInTheDocument();
13
+ });
14
+ it('renders with inline-block display style', () => {
15
+ const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
16
+ expect(container.firstChild).toHaveStyle({ display: 'inline-block' });
17
+ });
18
+ it('renders with text-align center', () => {
19
+ const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
20
+ expect(container.firstChild).toHaveStyle({ textAlign: 'center' });
21
+ });
22
+ it('renders with width 100%', () => {
23
+ const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
24
+ expect(container.firstChild).toHaveStyle({ width: '100%' });
25
+ });
26
+ it('renders with position relative', () => {
27
+ const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
28
+ expect(container.firstChild).toHaveStyle({ position: 'relative' });
29
+ });
30
+ it('renders a nested div element', () => {
31
+ const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
32
+ const outerDiv = container.firstChild;
33
+ expect(outerDiv.querySelector('div')).toBeInTheDocument();
34
+ });
35
+ it('renders the translated divider text', () => {
36
+ const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
37
+ // The divider uses i18n key 'divider', text should be present
38
+ const innerDiv = container.querySelector('div > div');
39
+ expect(innerDiv).toBeInTheDocument();
40
+ expect(innerDiv.textContent).toBeTruthy();
41
+ });
42
+ });
43
+ });
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render } from '@testing-library/react';
4
+ import TypingIndicator from './TypingIndicator';
5
+ describe('TypingIndicator', () => {
6
+ describe('rendering', () => {
7
+ it('renders exactly three circle icons', () => {
8
+ const { container } = render(_jsx(TypingIndicator, {}));
9
+ // Each Circle icon renders as an <svg> element
10
+ const svgs = container.querySelectorAll('svg');
11
+ expect(svgs).toHaveLength(3);
12
+ });
13
+ it('renders a root Stack element', () => {
14
+ const { container } = render(_jsx(TypingIndicator, {}));
15
+ expect(container.firstChild).toBeInTheDocument();
16
+ });
17
+ it('renders with flex row layout', () => {
18
+ const { container } = render(_jsx(TypingIndicator, {}));
19
+ // MUI Stack with direction="row" uses flexbox
20
+ const root = container.firstChild;
21
+ expect(root).toHaveStyle({ display: 'flex', flexDirection: 'row' });
22
+ });
23
+ });
24
+ describe('accessibility', () => {
25
+ it('renders the circles as SVG elements', () => {
26
+ const { container } = render(_jsx(TypingIndicator, {}));
27
+ const svgs = container.querySelectorAll('svg');
28
+ svgs.forEach(svg => {
29
+ expect(svg.tagName.toLowerCase()).toBe('svg');
30
+ });
31
+ });
32
+ });
33
+ });
@@ -1,5 +1,9 @@
1
1
  import type { FC } from 'react';
2
2
  declare const ConfirmDeleteModal: FC<{
3
3
  onConfirm: () => void;
4
+ title?: string;
5
+ description?: string;
6
+ preferDelete?: boolean;
7
+ preferCancel?: boolean;
4
8
  }>;
5
9
  export default ConfirmDeleteModal;
@@ -3,13 +3,15 @@ import { Button, Stack, Typography } from '@mui/material';
3
3
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
4
4
  import { useCallback, useContext } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
- const ConfirmDeleteModal = ({ onConfirm }) => {
6
+ const ConfirmDeleteModal = ({ onConfirm, title, description, preferDelete, preferCancel }) => {
7
7
  const { t } = useTranslation();
8
8
  const { close } = useContext(ModalContext);
9
9
  const handleConfirm = useCallback(() => {
10
10
  onConfirm();
11
11
  close();
12
12
  }, [close, onConfirm]);
13
- return (_jsxs(Stack, { spacing: 2, p: 2, alignItems: "start", sx: { minWidth: '500px' }, children: [_jsx(Typography, { variant: "h4", children: t('modal.confirm.delete.title') }), _jsx(Typography, { children: t('modal.confirm.delete.description') }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: "outlined", onClick: close, children: t('cancel') }), _jsx(Button, { variant: "outlined", onClick: handleConfirm, children: t('confirm') })] })] }));
13
+ const modalTitle = title ?? t('modal.confirm.delete.title');
14
+ const modalDesc = description ?? t('modal.confirm.delete.description');
15
+ return (_jsxs(Stack, { spacing: 2, p: 2, alignItems: "start", sx: { minWidth: '500px' }, children: [_jsx(Typography, { variant: "h4", children: modalTitle }), _jsx(Typography, { children: modalDesc }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: preferCancel ? 'contained' : 'outlined', onClick: close, children: t('button.cancel') }), _jsx(Button, { variant: preferDelete ? 'contained' : 'outlined', onClick: handleConfirm, children: t('button.delete') })] })] }));
14
16
  };
15
17
  export default ConfirmDeleteModal;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Chip, Divider, Grid, Stack, Tooltip, Typography, avatarClasses, iconButtonClasses, useTheme } from '@mui/material';
2
+ import { OpenInNew } from '@mui/icons-material';
3
+ import { Box, Chip, Divider, Grid, Stack, Tooltip, Typography, avatarClasses, chipClasses, iconButtonClasses, useTheme } from '@mui/material';
3
4
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
4
5
  import { uniq } from 'lodash-es';
5
6
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
@@ -75,7 +76,7 @@ const HitBanner = ({ hit, lazy = false, layout = HitLayout.NORMAL, showAssigned
75
76
  ], children: [t(i18nKey), ":"] }), (Array.isArray(value) ? value : [value]).map(val => (_jsx(PluginTypography, { component: "span", context: "banner", variant: textVariant, noWrap: compressed, textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, value: val, field: field, obj: hit }, val)))] }));
76
77
  return compressed ? (_jsx(Tooltip, { title: Array.isArray(value) ? (_jsx("div", { children: value.map(_indicator => (_jsx("p", { style: { margin: 0, padding: 0 }, children: _indicator }, _indicator))) })) : (value), children: _children })) : (_children);
77
78
  }, [compressed, hit, t, textVariant]);
78
- return (_jsxs(Box, { display: "grid", gridTemplateColumns: "minmax(0, auto) minmax(0, 1fr) minmax(0, auto)", alignItems: "stretch", sx: { width: '100%', ml: 0, overflow: 'hidden' }, children: [leftBox, _jsxs(Stack, { sx: {
79
+ return (_jsxs(Box, { display: "grid", gridTemplateColumns: "minmax(0, auto) minmax(0, 1fr) minmax(0, auto)", alignItems: "stretch", sx: { width: '100%', ml: 0, overflow: 'hidden', textDecoration: 'none', color: 'text.primary' }, component: "a", href: `/hits/${hit?.howler.id}`, onClick: e => e.preventDefault(), children: [leftBox, _jsxs(Stack, { sx: {
79
80
  height: '100%',
80
81
  padding: theme.spacing(1),
81
82
  gridColumn: { xs: 'span 3', sm: 'span 1', md: 'span 1' }
@@ -96,6 +97,8 @@ const HitBanner = ({ hit, lazy = false, layout = HitLayout.NORMAL, showAssigned
96
97
  width: theme.spacing(3)
97
98
  }
98
99
  }
99
- ], children: [_jsx(HitTimestamp, { hit: hit, layout: layout }), showAssigned && _jsx(Assigned, { hit: hit, layout: layout }), _jsxs(Stack, { direction: "row", spacing: layout !== HitLayout.COMFY ? 0.5 : 1, children: [_jsx(EscalationChip, { hit: hit, layout: layout }), ['in-progress', 'on-hold'].includes(hit.howler.status) && (_jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, color: "primary" }))] }), hit.howler.related && _jsx(RelatedRecords, { hit: hit }), howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.status`, { hit, layout }))] })] }));
100
+ ], children: [_jsx(HitTimestamp, { hit: hit, layout: layout }), _jsx(Assigned, { hit: hit, layout: layout, showAssigned: showAssigned }), hit.howler.links?.[0]?.href && (_jsx(Chip, { icon: _jsx(OpenInNew, {}), label: hit.howler.links[0].title || t('hit.header.link'), size: layout !== HitLayout.COMFY ? 'small' : 'medium', component: "a", href: hit.howler.links[0].href, target: "_blank", rel: "noopener noreferrer", sx: { [`.${chipClasses.label}`]: { cursor: 'pointer !important' } }, onClick: e => {
101
+ e.stopPropagation();
102
+ } })), _jsxs(Stack, { direction: "row", spacing: layout !== HitLayout.COMFY ? 0.5 : 1, children: [_jsx(EscalationChip, { hit: hit, layout: layout }), ['in-progress', 'on-hold'].includes(hit.howler.status) && (_jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, color: "primary" }))] }), hit.howler.related && _jsx(RelatedRecords, { hit: hit }), howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.status`, { hit, layout }))] })] }));
100
103
  };
101
104
  export default HitBanner;
@@ -12,7 +12,7 @@ const HitLinks = ({ hit, analytic, dossiers = [] }) => {
12
12
  dossiers.filter(_dossier => _dossier.pivots?.length > 0).length > 0) && (_jsxs(Grid, { container: true, spacing: 1, pr: 2, sx: { [`& .${gridClasses.item}`]: { display: 'flex' } }, children: [hit?.howler?.links?.length > 0 &&
13
13
  uniqBy(hit.howler.links, 'href')
14
14
  .slice(0, 3)
15
- .map(l => (_jsx(Grid, { item: true, children: _jsx(RelatedLink, { compact: true, ...l }) }, l.href))), sortBy(dossiers.flatMap(_dossier => _dossier.pivots ?? []), `label.${i18n.language}`).map((_pivot, index) => (
15
+ .map(l => (_jsx(Grid, { item: true, children: _jsx(RelatedLink, { compact: true, ...l, target: "_blank", rel: "noopener noreferrer" }) }, l.href))), sortBy(dossiers.flatMap(_dossier => _dossier.pivots ?? []), `label.${i18n.language}`).map((_pivot, index) => (
16
16
  // eslint-disable-next-line react/no-array-index-key
17
17
  _jsx(Grid, { item: true, children: _jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }) }, _pivot.value + index))), analytic?.notebooks?.length > 0 && (_jsx(Grid, { item: true, children: _jsx(HitNotebooks, { analytic: analytic, hit: hit, compact: true }) }))] })));
18
18
  };
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Analytics, InfoOutlined } from '@mui/icons-material';
3
3
  import { Alert, AlertTitle, Autocomplete, Box, Button, Chip, CircularProgress, Divider, Fade, Grid, LinearProgress, Stack, TextField, Tooltip, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
@@ -24,6 +24,7 @@ const HitSummary = ({ response, onStart, onComplete }) => {
24
24
  const { hitFields } = useContext(FieldContext);
25
25
  const { showErrorMessage } = useMySnackbar();
26
26
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
27
+ const [showHitSummaryGraph] = useMyLocalStorageItem(StorageKey.SHOW_HIT_SUMMARY_GRAPH, true);
27
28
  const { getMatchingTemplate } = useMatchers();
28
29
  const searching = useContextSelector(RecordSearchContext, ctx => ctx.searching);
29
30
  const error = useContextSelector(RecordSearchContext, ctx => ctx.error);
@@ -131,7 +132,7 @@ const HitSummary = ({ response, onStart, onComplete }) => {
131
132
  performAggregation();
132
133
  // eslint-disable-next-line react-hooks/exhaustive-deps
133
134
  }, [query, views, searching, error]);
134
- return (_jsxs(Stack, { sx: { mx: 2, height: '100%' }, spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('hit.summary.aggregate.title') }), _jsx(Divider, { flexItem: true }), _jsx(HitGraph, {}), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { sx: { overflow: 'auto', marginTop: '0 !important' }, pt: 1, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(Autocomplete, { fullWidth: true, multiple: true, sx: { minWidth: '175px' }, size: "small", value: customKeys, options: hitFields.map(_field => _field.key), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.summary.adhoc') }), onChange: (_, value) => setCustomKeys(value) }), _jsx(Button, { variant: "outlined", startIcon: loading ? _jsx(CircularProgress, { size: 20, sx: { ml: 1 } }) : _jsx(Analytics, { sx: { ml: 1 } }), disabled: loading, onClick: () => performAggregation(), children: t('button.aggregate') })] }), isEmpty(aggregateResults) && (_jsxs(Alert, { severity: "info", variant: "outlined", children: [_jsx(AlertTitle, { children: t('hit.summary.aggregate.nokeys.title') }), t('hit.summary.aggregate.nokeys.description')] })), loading && _jsx(LinearProgress, { sx: { minHeight: '4px' } }), Object.keys(aggregateResults)
135
+ return (_jsxs(Stack, { sx: { mx: 2, height: '100%' }, spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('hit.summary.aggregate.title') }), _jsx(Divider, { flexItem: true }), showHitSummaryGraph && (_jsxs(_Fragment, { children: [_jsx(HitGraph, {}), _jsx(Divider, { flexItem: true })] })), _jsxs(Stack, { sx: { overflow: 'auto', marginTop: '0 !important' }, pt: 1, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(Autocomplete, { fullWidth: true, multiple: true, sx: { minWidth: '175px' }, size: "small", value: customKeys, options: hitFields.map(_field => _field.key), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.summary.adhoc') }), onChange: (_, value) => setCustomKeys(value) }), _jsx(Button, { variant: "outlined", startIcon: loading ? _jsx(CircularProgress, { size: 20, sx: { ml: 1 } }) : _jsx(Analytics, { sx: { ml: 1 } }), disabled: loading, onClick: () => performAggregation(), children: t('button.aggregate') })] }), isEmpty(aggregateResults) && (_jsxs(Alert, { severity: "info", variant: "outlined", children: [_jsx(AlertTitle, { children: t('hit.summary.aggregate.nokeys.title') }), t('hit.summary.aggregate.nokeys.description')] })), loading && _jsx(LinearProgress, { sx: { minHeight: '4px' } }), Object.keys(aggregateResults)
135
136
  .filter(key => !isEmpty(aggregateResults[key]))
136
137
  .flatMap(key => [
137
138
  _jsx(Fade, { in: true, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "body1", children: key }, key + '-title'), keyCounts[key]?.count < 0 ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", t('hit.summary.adhoc.custom'), ")"] })) : (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", keyCounts[key]?.count ?? '?', " ", t('references'), ")"] })), _jsx(Tooltip, { title: _jsxs(Stack, { children: [_jsx(Typography, { variant: "caption", children: t('hit.summary.aggregate.sources') }), keyCounts[key]?.sources.map(source => (_jsx(Typography, { variant: "caption", children: source }, source))) ?? '?'] }), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }) }, key + '-refs'),
@@ -13,7 +13,7 @@ const AnalyticLink = ({ hit, lazy = false, compressed, alignSelf = 'start' }) =>
13
13
  getMatchingAnalytic(hit).then(analytic => setAnalyticId(analytic?.analytic_id));
14
14
  // eslint-disable-next-line react-hooks/exhaustive-deps
15
15
  }, [hit?.howler.analytic]);
16
- return (_jsxs(Typography, { variant: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf, '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, onAuxClick: e => {
16
+ return (_jsxs(Typography, { variant: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf, '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, target: "_blank", rel: "noopener noreferrer", onAuxClick: e => {
17
17
  e.stopPropagation();
18
18
  }, onClick: e => {
19
19
  e.stopPropagation();
@@ -5,5 +5,7 @@ declare const Assigned: FC<{
5
5
  hit: Hit;
6
6
  layout: HitLayout;
7
7
  hideLabel?: boolean;
8
+ showAssessor?: boolean;
9
+ showAssigned?: boolean;
8
10
  }>;
9
11
  export default Assigned;
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { avatarClasses, AvatarGroup, Chip, Stack } from '@mui/material';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { avatarClasses, AvatarGroup, Chip, Divider, Stack, Typography } from '@mui/material';
3
3
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
4
4
  import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
5
5
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
@@ -7,21 +7,23 @@ import { uniq } from 'lodash-es';
7
7
  import { useContext } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
  import { HitLayout } from '../HitLayout';
10
- const Assigned = ({ hit, layout, hideLabel = false }) => {
10
+ const AvatarChip = ({ userId, noUser, placeholder, layout, hideLabel }) => {
11
+ const userAvatar = (_jsx(HowlerAvatar, { userId: userId, sx: { height: layout !== HitLayout.COMFY ? 24 : 32, width: layout !== HitLayout.COMFY ? 24 : 32 } }));
12
+ return hideLabel ? (userAvatar) : (_jsx(Chip, { variant: "outlined", sx: {
13
+ width: 'fit-content',
14
+ '& .MuiChip-icon': {
15
+ marginLeft: 0
16
+ }
17
+ }, icon: userAvatar, label: userId && userId !== noUser ? userId : placeholder, size: layout !== HitLayout.COMFY ? 'small' : 'medium' }));
18
+ };
19
+ const Assigned = ({ hit, layout, hideLabel = false, showAssessor = false, showAssigned = false }) => {
11
20
  const { t } = useTranslation();
12
21
  const { user } = useAppUser();
13
22
  const { viewers } = useContext(SocketContext);
14
23
  const hitViewers = uniq(viewers[hit?.howler?.id] ?? []).filter(viewer => viewer !== user.username);
15
- const userAvatar = (_jsx(HowlerAvatar, { userId: hit.howler.assignment, sx: { height: layout !== HitLayout.COMFY ? 24 : 32, width: layout !== HitLayout.COMFY ? 24 : 32 } }));
16
- return (_jsxs(Stack, { direction: "row", spacing: 0.5, children: [hideLabel ? (userAvatar) : (_jsx(Chip, { variant: "outlined", sx: {
17
- width: 'fit-content',
18
- '& .MuiChip-icon': {
19
- marginLeft: 0
20
- }
21
- }, icon: userAvatar, label: !hideLabel &&
22
- (hit?.howler.assignment !== 'unassigned'
23
- ? hit?.howler.assignment
24
- : t('app.drawer.hit.assignment.unassigned.name')), size: layout !== HitLayout.COMFY ? 'small' : 'medium' })), _jsx(AvatarGroup, { max: 3, sx: { [`.${avatarClasses.root}`]: { border: 0, marginLeft: 0.5 } }, componentsProps: {
24
+ const assessorVisible = showAssessor || hit.howler.assessment != null;
25
+ const assigneeVisible = !hit.howler.assessor && (showAssigned || hit.howler.assignment !== 'unassigned');
26
+ return (_jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsxs(Stack, { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", alignItems: "center", columnGap: 0.5, rowGap: 0.25, children: [assigneeVisible && (_jsxs(_Fragment, { children: [!hideLabel && _jsxs(Typography, { variant: "caption", children: [t('app.drawer.hit.assignment.assignee'), ":"] }), _jsx(AvatarChip, { userId: hit?.howler.assignment, noUser: "unassigned", placeholder: t('app.drawer.hit.assignment.unassigned.name'), layout: layout, hideLabel: hideLabel })] })), assessorVisible && (_jsxs(_Fragment, { children: [!hideLabel && _jsxs(Typography, { variant: "caption", children: [t('app.drawer.hit.assessment.assessor'), ":"] }), _jsx(AvatarChip, { userId: hit.howler.assessor, noUser: "unknown", placeholder: t('app.drawer.hit.assessment.unknown.name'), layout: layout, hideLabel: hideLabel })] }))] }), hitViewers.length > 0 && hideLabel && _jsx(Divider, { orientation: "vertical", flexItem: true, variant: "middle" }), _jsx(AvatarGroup, { max: 3, sx: { [`.${avatarClasses.root}`]: { border: 0, marginLeft: 0.5 } }, componentsProps: {
25
27
  additionalAvatar: {
26
28
  sx: {
27
29
  height: layout !== HitLayout.COMFY ? 24 : 32,
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { AddCircleOutline, Assignment, CreateNewFolder, Edit, HowToVote, NoteAdd, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
3
3
  import api from '@cccsaurora/howler-ui/api';
4
+ import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks/useAppUser';
4
5
  import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
6
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
7
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
@@ -43,6 +44,7 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
43
44
  const { t } = useTranslation();
44
45
  const { dispatchApi } = useMyApi();
45
46
  const { executeAction } = useMyActionFunctions();
47
+ const appUser = useAppUser();
46
48
  const { config } = useContext(ApiConfigContext);
47
49
  const { showModal } = useContext(ModalContext);
48
50
  const pluginStore = usePluginStore();
@@ -62,6 +64,14 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
62
64
  : [], [record, selectedRecords]);
63
65
  const hits = useMemo(() => records.filter(isHit), [records]);
64
66
  const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
67
+ /**
68
+ * Checks if the current user has permission to run actions.
69
+ * Users must have one of the automation or actionrunner roles, or be an admin.
70
+ */
71
+ const canRunActions = useCallback(() => {
72
+ const roles = ['admin', 'automation_advanced', 'automation_basic', 'actionrunner_advanced', 'actionrunner_basic'];
73
+ return roles.some((role) => appUser.user?.roles?.includes(role));
74
+ }, [appUser.user?.roles]);
65
75
  /**
66
76
  * Called by ContextMenu after the menu is positioned and opened.
67
77
  * Identifies the clicked record and fetches available actions.
@@ -69,8 +79,10 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
69
79
  const onOpen = useCallback(async (event) => {
70
80
  const _id = getSelectedId(event);
71
81
  setId(_id);
72
- const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*' }), { throwError: false }))
73
- ?.items;
82
+ // TODO: Bumping the number of rows is a temporary fix - we'll need to improve this.
83
+ const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*', rows: 100, sort: 'name asc' }), {
84
+ throwError: false
85
+ }))?.items;
74
86
  if (_actions) {
75
87
  setActions(_actions);
76
88
  }
@@ -163,7 +175,7 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
163
175
  id: 'actions',
164
176
  icon: _jsx(SettingsSuggest, {}),
165
177
  label: t('route.actions.change'),
166
- disabled: actions.length < 1,
178
+ disabled: actions.length < 1 || !canRunActions(),
167
179
  items: actions.map(action => ({
168
180
  key: action.action_id,
169
181
  label: action.name,
@@ -8,6 +8,16 @@ import { vi } from 'vitest';
8
8
  // Mock API
9
9
  vi.mock('api', { spy: true });
10
10
  setupContextSelectorMock();
11
+ // Mock useAppUser hook
12
+ const mockUseAppUser = vi.hoisted(() => vi.fn(() => ({
13
+ user: {
14
+ username: 'test-user',
15
+ roles: ['automation_basic', 'actionrunner_basic']
16
+ }
17
+ })));
18
+ vi.mock('commons/components/app/hooks/useAppUser', () => ({
19
+ useAppUser: mockUseAppUser
20
+ }));
11
21
  // Mock react-router-dom
12
22
  const mockNavigate = vi.fn();
13
23
  vi.mock('react-router-dom', async () => {
@@ -948,4 +958,90 @@ describe('HitContextMenu', () => {
948
958
  });
949
959
  });
950
960
  });
961
+ describe('Role-Based Action Permissions', () => {
962
+ afterEach(() => {
963
+ // Reset to default user with required roles
964
+ mockUseAppUser.mockReturnValue({
965
+ user: {
966
+ username: 'test-user',
967
+ roles: ['automation_basic', 'actionrunner_basic']
968
+ }
969
+ });
970
+ });
971
+ it('should disable actions menu when user lacks required roles', async () => {
972
+ mockUseAppUser.mockReturnValue({
973
+ user: {
974
+ username: 'test-user',
975
+ roles: ['user', 'viewer']
976
+ }
977
+ });
978
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
979
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
980
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
981
+ act(() => {
982
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
983
+ fireEvent.contextMenu(contextMenuWrapper);
984
+ });
985
+ await waitFor(() => {
986
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
987
+ expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
988
+ });
989
+ });
990
+ it('should enable actions menu when user has automation_basic role', async () => {
991
+ mockUseAppUser.mockReturnValue({
992
+ user: {
993
+ username: 'test-user',
994
+ roles: ['automation_basic']
995
+ }
996
+ });
997
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
998
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
999
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1000
+ act(() => {
1001
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
1002
+ fireEvent.contextMenu(contextMenuWrapper);
1003
+ });
1004
+ await waitFor(() => {
1005
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
1006
+ expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
1007
+ });
1008
+ });
1009
+ it('should enable actions menu when user has actionrunner_advanced role', async () => {
1010
+ mockUseAppUser.mockReturnValue({
1011
+ user: {
1012
+ username: 'test-user',
1013
+ roles: ['actionrunner_advanced']
1014
+ }
1015
+ });
1016
+ const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
1017
+ mockDispatchApi.mockResolvedValue({ items: mockActions });
1018
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1019
+ act(() => {
1020
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
1021
+ fireEvent.contextMenu(contextMenuWrapper);
1022
+ });
1023
+ await waitFor(() => {
1024
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
1025
+ expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
1026
+ });
1027
+ });
1028
+ it('should still disable actions menu when user has roles but no actions available', async () => {
1029
+ mockUseAppUser.mockReturnValue({
1030
+ user: {
1031
+ username: 'test-user',
1032
+ roles: ['automation_advanced']
1033
+ }
1034
+ });
1035
+ mockDispatchApi.mockResolvedValue({ items: [] });
1036
+ rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
1037
+ act(() => {
1038
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
1039
+ fireEvent.contextMenu(contextMenuWrapper);
1040
+ });
1041
+ await waitFor(() => {
1042
+ const actionsMenuItem = screen.getByTestId('actions-menu-item');
1043
+ expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
1044
+ });
1045
+ });
1046
+ });
951
1047
  });
@@ -1,4 +1,4 @@
1
- type DispatchApiConfig = {
1
+ export type DispatchApiConfig = {
2
2
  throwError?: boolean;
3
3
  logError?: boolean;
4
4
  showError?: boolean;
@@ -285,7 +285,8 @@ const useMyPreferences = () => {
285
285
  rightBeforeSearch: (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", pr: 1, children: [rightItems.map(item => (_jsx(Fragment, { children: item.component }, item.id))), _jsx(Classification, {})] }))
286
286
  },
287
287
  leftnav: {
288
- elements: MENU_ITEMS
288
+ elements: MENU_ITEMS,
289
+ width: 280
289
290
  }
290
291
  }), [USER_MENU_ITEMS, ADMIN_MENU_ITEMS, MENU_ITEMS, leftItems, rightItems]);
291
292
  };
@@ -0,0 +1,3 @@
1
+ import type { FC } from 'react';
2
+ declare const PermissionDeniedPage: FC;
3
+ export default PermissionDeniedPage;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PersonOff } from '@mui/icons-material';
3
+ import { Box, Typography } from '@mui/material';
4
+ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
5
+ import { useTranslation } from 'react-i18next';
6
+ const PermissionDeniedPage = () => {
7
+ const { t } = useTranslation();
8
+ return (_jsxs(PageCenter, { width: "75%", children: [_jsx(Box, { pt: 6, textAlign: "center", fontSize: 200, children: _jsx(PersonOff, { color: "secondary", fontSize: "inherit" }) }), _jsx(Box, { pb: 2, children: _jsx(Typography, { variant: "h2", children: t('page.403.title') }) }), _jsx(Box, { children: _jsx(Typography, { variant: "h5", children: t('page.403.description') }) })] }));
9
+ };
10
+ export default PermissionDeniedPage;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import { I18nextProvider } from 'react-i18next';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import PermissionDeniedPage from './403';
8
+ vi.mock('commons/components/pages/PageCenter', () => ({
9
+ default: ({ children }) => _jsx("div", { id: "page-center", children: children })
10
+ }));
11
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
12
+ describe('PermissionDeniedPage (403)', () => {
13
+ it('should render the 403 title and description', () => {
14
+ render(_jsx(PermissionDeniedPage, {}), { wrapper: Wrapper });
15
+ expect(screen.getByText('403: Access Forbidden')).toBeInTheDocument();
16
+ expect(screen.getByText('You do not have permission to access this page.')).toBeInTheDocument();
17
+ });
18
+ it('should render inside PageCenter', () => {
19
+ render(_jsx(PermissionDeniedPage, {}), { wrapper: Wrapper });
20
+ expect(screen.getByTestId('page-center')).toBeInTheDocument();
21
+ });
22
+ it('should render the PersonOff icon', () => {
23
+ render(_jsx(PermissionDeniedPage, {}), { wrapper: Wrapper });
24
+ // MUI SVG icons have data-testid attribute
25
+ expect(document.querySelector('[data-testid="PersonOffIcon"]')).toBeInTheDocument();
26
+ });
27
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import { I18nextProvider } from 'react-i18next';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import NotFoundPage from './404';
8
+ vi.mock('commons/components/pages/PageCenter', () => ({
9
+ default: ({ children }) => _jsx("div", { id: "page-center", children: children })
10
+ }));
11
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
12
+ describe('NotFoundPage (404)', () => {
13
+ it('should render the 404 title and description', () => {
14
+ render(_jsx(NotFoundPage, {}), { wrapper: Wrapper });
15
+ expect(screen.getByText('404: Not found')).toBeInTheDocument();
16
+ expect(screen.getByText('The page you are looking for cannot be found...')).toBeInTheDocument();
17
+ });
18
+ it('should render inside PageCenter', () => {
19
+ render(_jsx(NotFoundPage, {}), { wrapper: Wrapper });
20
+ expect(screen.getByTestId('page-center')).toBeInTheDocument();
21
+ });
22
+ it('should render the LinkOff icon', () => {
23
+ render(_jsx(NotFoundPage, {}), { wrapper: Wrapper });
24
+ expect(document.querySelector('[data-testid="LinkOffIcon"]')).toBeInTheDocument();
25
+ });
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import { I18nextProvider } from 'react-i18next';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import ErrorBoundary from './ErrorBoundary';
8
+ vi.mock('commons/components/pages/PageCenter', () => ({
9
+ default: ({ children }) => _jsx("div", { id: "page-center", children: children })
10
+ }));
11
+ vi.mock('react-pluggable', () => ({
12
+ usePluginStore: () => ({
13
+ executeFunction: vi.fn(() => null)
14
+ })
15
+ }));
16
+ vi.mock('plugins/store', () => ({
17
+ default: { plugins: [] }
18
+ }));
19
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
20
+ const ThrowingComponent = ({ error }) => {
21
+ throw error;
22
+ };
23
+ describe('ErrorBoundary', () => {
24
+ it('should render children when no error occurs', () => {
25
+ render(_jsx(ErrorBoundary, { children: _jsx("div", { children: "Child content" }) }), { wrapper: Wrapper });
26
+ expect(screen.getByText('Child content')).toBeInTheDocument();
27
+ });
28
+ it('should render error UI when a child throws', () => {
29
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
30
+ const testError = new Error('Test error message');
31
+ testError.stack = 'Error: Test error message\n at TestComponent';
32
+ render(_jsx(ErrorBoundary, { children: _jsx(ThrowingComponent, { error: testError }) }), { wrapper: Wrapper });
33
+ expect(screen.getByText('Test error message')).toBeInTheDocument();
34
+ consoleSpy.mockRestore();
35
+ });
36
+ it('should display the error page title when an error occurs', () => {
37
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
38
+ const testError = new Error('Something went wrong');
39
+ render(_jsx(ErrorBoundary, { children: _jsx(ThrowingComponent, { error: testError }) }), { wrapper: Wrapper });
40
+ expect(screen.getByText('Application Stopped Working')).toBeInTheDocument();
41
+ consoleSpy.mockRestore();
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import i18n from '@cccsaurora/howler-ui/i18n';
5
+ import { I18nextProvider } from 'react-i18next';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import ErrorOccured from './ErrorOccured';
8
+ vi.mock('commons/components/pages/PageCenter', () => ({
9
+ default: ({ children }) => _jsx("div", { id: "page-center", children: children })
10
+ }));
11
+ const mockExecuteFunction = vi.fn(() => null);
12
+ vi.mock('react-pluggable', () => ({
13
+ usePluginStore: () => ({
14
+ executeFunction: mockExecuteFunction
15
+ })
16
+ }));
17
+ vi.mock('plugins/store', () => ({
18
+ default: { plugins: ['TestPlugin'] }
19
+ }));
20
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
21
+ describe('ErrorOccured', () => {
22
+ it('should render the error title and description', () => {
23
+ render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
24
+ expect(screen.getByText('Application Stopped Working')).toBeInTheDocument();
25
+ expect(screen.getByText('The application stopped working suddenly. If the problem persists please reach out on teams.')).toBeInTheDocument();
26
+ });
27
+ it('should render inside PageCenter', () => {
28
+ render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
29
+ expect(screen.getByTestId('page-center')).toBeInTheDocument();
30
+ });
31
+ it('should execute plugin support functions', () => {
32
+ render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
33
+ expect(mockExecuteFunction).toHaveBeenCalledWith('TestPlugin.support');
34
+ });
35
+ });
@@ -0,0 +1 @@
1
+ export {};