@eeacms/volto-eea-chatbot 1.0.9

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 (133) hide show
  1. package/.coverage.babel.config.js +9 -0
  2. package/.eslintrc.js +68 -0
  3. package/.husky/pre-commit +2 -0
  4. package/.release-it.json +17 -0
  5. package/AGENTS.md +89 -0
  6. package/CHANGELOG.md +770 -0
  7. package/DEVELOP.md +124 -0
  8. package/LICENSE.md +9 -0
  9. package/README.md +170 -0
  10. package/RELEASE.md +74 -0
  11. package/TESTING.md +5 -0
  12. package/babel.config.js +17 -0
  13. package/bootstrap +41 -0
  14. package/cypress.config.js +27 -0
  15. package/docker-compose.yml +32 -0
  16. package/jest-addon.config.js +465 -0
  17. package/jest.setup.js +65 -0
  18. package/locales/de/LC_MESSAGES/volto.po +14 -0
  19. package/locales/en/LC_MESSAGES/volto.po +14 -0
  20. package/locales/it/LC_MESSAGES/volto.po +14 -0
  21. package/locales/ro/LC_MESSAGES/volto.po +14 -0
  22. package/locales/volto.pot +16 -0
  23. package/package.json +98 -0
  24. package/razzle.extend.js +40 -0
  25. package/src/ChatBlock/ChatBlockEdit.jsx +46 -0
  26. package/src/ChatBlock/ChatBlockView.jsx +21 -0
  27. package/src/ChatBlock/chat/AIMessage.tsx +566 -0
  28. package/src/ChatBlock/chat/ChatMessage.tsx +35 -0
  29. package/src/ChatBlock/chat/ChatWindow.tsx +288 -0
  30. package/src/ChatBlock/chat/UserMessage.tsx +27 -0
  31. package/src/ChatBlock/chat/index.ts +4 -0
  32. package/src/ChatBlock/components/AutoResizeTextarea.jsx +67 -0
  33. package/src/ChatBlock/components/BlinkingDot.tsx +3 -0
  34. package/src/ChatBlock/components/ChatMessageFeedback.jsx +77 -0
  35. package/src/ChatBlock/components/EmptyState.jsx +70 -0
  36. package/src/ChatBlock/components/FeedbackModal.jsx +125 -0
  37. package/src/ChatBlock/components/HalloumiFeedback.jsx +126 -0
  38. package/src/ChatBlock/components/Icon.tsx +35 -0
  39. package/src/ChatBlock/components/QualityCheckToggle.jsx +26 -0
  40. package/src/ChatBlock/components/RelatedQuestions.jsx +59 -0
  41. package/src/ChatBlock/components/Source.jsx +93 -0
  42. package/src/ChatBlock/components/SourceChip.tsx +55 -0
  43. package/src/ChatBlock/components/Spinner.jsx +3 -0
  44. package/src/ChatBlock/components/UserActionsToolbar.jsx +44 -0
  45. package/src/ChatBlock/components/WebResultIcon.tsx +42 -0
  46. package/src/ChatBlock/components/markdown/Citation.jsx +70 -0
  47. package/src/ChatBlock/components/markdown/ClaimModal.jsx +98 -0
  48. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +172 -0
  49. package/src/ChatBlock/components/markdown/RenderClaimView.jsx +96 -0
  50. package/src/ChatBlock/components/markdown/colors.js +29 -0
  51. package/src/ChatBlock/components/markdown/colors.less +52 -0
  52. package/src/ChatBlock/components/markdown/colors.test.js +69 -0
  53. package/src/ChatBlock/components/markdown/index.js +115 -0
  54. package/src/ChatBlock/fonts/DejaVuSans.ttf +0 -0
  55. package/src/ChatBlock/hocs/withOnyxData.jsx +46 -0
  56. package/src/ChatBlock/hooks/index.ts +7 -0
  57. package/src/ChatBlock/hooks/useChatController.ts +333 -0
  58. package/src/ChatBlock/hooks/useChatStreaming.ts +82 -0
  59. package/src/ChatBlock/hooks/useDeepCompareMemoize.js +17 -0
  60. package/src/ChatBlock/hooks/useMarked.js +44 -0
  61. package/src/ChatBlock/hooks/useQualityMarkers.js +119 -0
  62. package/src/ChatBlock/hooks/useScrollonStream.ts +131 -0
  63. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +80 -0
  64. package/src/ChatBlock/index.js +32 -0
  65. package/src/ChatBlock/packets/MultiToolRenderer.tsx +235 -0
  66. package/src/ChatBlock/packets/RendererComponent.tsx +115 -0
  67. package/src/ChatBlock/packets/index.ts +4 -0
  68. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +63 -0
  69. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +59 -0
  70. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +62 -0
  71. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +172 -0
  72. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +122 -0
  73. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +323 -0
  74. package/src/ChatBlock/packets/renderers/index.ts +6 -0
  75. package/src/ChatBlock/schema.js +403 -0
  76. package/src/ChatBlock/services/index.ts +3 -0
  77. package/src/ChatBlock/services/messageProcessor.ts +348 -0
  78. package/src/ChatBlock/services/packetUtils.ts +48 -0
  79. package/src/ChatBlock/services/streamingService.ts +342 -0
  80. package/src/ChatBlock/style.less +1881 -0
  81. package/src/ChatBlock/tests/AIMessage.test.jsx +95 -0
  82. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +49 -0
  83. package/src/ChatBlock/tests/BlinkingDot.test.jsx +71 -0
  84. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +73 -0
  85. package/src/ChatBlock/tests/Citation.test.jsx +107 -0
  86. package/src/ChatBlock/tests/EmptyState.test.jsx +137 -0
  87. package/src/ChatBlock/tests/FeedbackModal.test.jsx +138 -0
  88. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +94 -0
  89. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +105 -0
  90. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +215 -0
  91. package/src/ChatBlock/tests/Source.test.jsx +79 -0
  92. package/src/ChatBlock/tests/Spinner.test.jsx +18 -0
  93. package/src/ChatBlock/tests/index.test.js +51 -0
  94. package/src/ChatBlock/tests/messageProcessor.test.jsx +154 -0
  95. package/src/ChatBlock/tests/schema.test.js +166 -0
  96. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +107 -0
  97. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +151 -0
  98. package/src/ChatBlock/types/cssmodules.d.ts +7 -0
  99. package/src/ChatBlock/types/interfaces.ts +154 -0
  100. package/src/ChatBlock/types/slate.d.ts +3 -0
  101. package/src/ChatBlock/types/streamingModels.ts +267 -0
  102. package/src/ChatBlock/types/volto.d.ts +3 -0
  103. package/src/ChatBlock/utils/citations.ts +25 -0
  104. package/src/ChatBlock/utils/index.tsx +114 -0
  105. package/src/halloumi/README.md +1 -0
  106. package/src/halloumi/generative.js +219 -0
  107. package/src/halloumi/generative.test.js +88 -0
  108. package/src/halloumi/middleware.js +70 -0
  109. package/src/halloumi/postprocessing.js +273 -0
  110. package/src/halloumi/postprocessing.test.js +441 -0
  111. package/src/halloumi/preprocessing.js +115 -0
  112. package/src/halloumi/preprocessing.test.js +245 -0
  113. package/src/icons/bot.svg +1 -0
  114. package/src/icons/check.svg +1 -0
  115. package/src/icons/chevron.svg +3 -0
  116. package/src/icons/clear.svg +1 -0
  117. package/src/icons/copy.svg +1 -0
  118. package/src/icons/done.svg +5 -0
  119. package/src/icons/external-link.svg +1 -0
  120. package/src/icons/file.svg +1 -0
  121. package/src/icons/glasses.svg +1 -0
  122. package/src/icons/globe.svg +1 -0
  123. package/src/icons/rotate.svg +1 -0
  124. package/src/icons/search.svg +5 -0
  125. package/src/icons/send.svg +1 -0
  126. package/src/icons/square-pen.svg +1 -0
  127. package/src/icons/stop.svg +9 -0
  128. package/src/icons/thumbs-down.svg +1 -0
  129. package/src/icons/thumbs-up.svg +1 -0
  130. package/src/icons/user.svg +1 -0
  131. package/src/index.js +58 -0
  132. package/src/middleware.js +250 -0
  133. package/tsconfig.json +40 -0
@@ -0,0 +1,94 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import HalloumiFeedback from '../components/HalloumiFeedback';
4
+
5
+ jest.mock('../components/Spinner', () => () => (
6
+ <div data-testid="spinner">Loading...</div>
7
+ ));
8
+
9
+ jest.mock('../components/Icon', () => ({ name }) => (
10
+ <img src={name} alt="icon" />
11
+ ));
12
+
13
+ jest.mock('../../icons/glasses.svg', () => 'glasses.svg');
14
+
15
+ jest.mock('@plone/volto-slate/editor/render', () => ({
16
+ serializeNodes: (nodes) => {
17
+ const visitTextNodes = (node) => {
18
+ if (Array.isArray(node)) return node.map(visitTextNodes).join('');
19
+ if (node && typeof node === 'object') {
20
+ if (node.text) return node.text;
21
+ if (node.children) return visitTextNodes(node.children);
22
+ }
23
+ return '';
24
+ };
25
+ return visitTextNodes(nodes);
26
+ },
27
+ }));
28
+
29
+ describe('HalloumiFeedback', () => {
30
+ const defaultProps = {
31
+ halloumiMessage: null,
32
+ isLoadingHalloumi: false,
33
+ markers: { claims: [{ score: 50, rationale: 'Some rationale' }] },
34
+ score: 75,
35
+ scoreColor: 'green',
36
+ onManualVerify: jest.fn(),
37
+ showVerifyClaimsButton: false,
38
+ sources: [],
39
+ };
40
+
41
+ it('renders fact-check button when showVerifyClaimsButton is true', () => {
42
+ render(<HalloumiFeedback {...defaultProps} showVerifyClaimsButton />);
43
+ const button = screen.getByRole('button', {
44
+ name: /Fact-check AI answer/i,
45
+ });
46
+ expect(button).toBeInTheDocument();
47
+ fireEvent.click(button);
48
+ expect(defaultProps.onManualVerify).toHaveBeenCalled();
49
+ expect(screen.getByText(/Please allow a few minutes/i)).toBeInTheDocument();
50
+ });
51
+
52
+ it('renders VerifyClaims message when loading and sources exist', () => {
53
+ render(
54
+ <HalloumiFeedback
55
+ {...defaultProps}
56
+ isLoadingHalloumi
57
+ sources={['doc1']}
58
+ showVerifyClaimsButton
59
+ />,
60
+ );
61
+ expect(screen.getByTestId('spinner')).toBeInTheDocument();
62
+ expect(screen.getByText(/Going through each claim/i)).toBeInTheDocument();
63
+ });
64
+
65
+ it('renders rationale message when no claims score', () => {
66
+ render(
67
+ <HalloumiFeedback
68
+ {...defaultProps}
69
+ markers={{ claims: [{ score: null, rationale: 'Failed to verify' }] }}
70
+ />,
71
+ );
72
+ expect(screen.getByText('Failed to verify')).toBeInTheDocument();
73
+ });
74
+
75
+ it('renders halloumiMessage with score replaced', () => {
76
+ const halloumiMessage = [
77
+ { type: 'paragraph', children: [{ text: 'Score: {score}' }] },
78
+ ];
79
+ render(
80
+ <HalloumiFeedback
81
+ {...defaultProps}
82
+ halloumiMessage={halloumiMessage}
83
+ score={88}
84
+ />,
85
+ );
86
+ expect(screen.getByText('Score: 88%')).toBeInTheDocument();
87
+ });
88
+
89
+ it('does not render anything extra when no special props', () => {
90
+ render(<HalloumiFeedback {...defaultProps} />);
91
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
92
+ expect(screen.queryByTestId('spinner')).not.toBeInTheDocument();
93
+ });
94
+ });
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import QualityCheckToggle from '../components/QualityCheckToggle';
5
+
6
+ describe('QualityCheckToggle', () => {
7
+ const mockSetEnabled = jest.fn();
8
+
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ });
12
+
13
+ it('renders correctly', () => {
14
+ const { container } = render(
15
+ <QualityCheckToggle
16
+ isEditMode={false}
17
+ enabled={true}
18
+ setEnabled={mockSetEnabled}
19
+ />,
20
+ );
21
+
22
+ expect(
23
+ container.querySelector('.quality-check-toggle'),
24
+ ).toBeInTheDocument();
25
+ });
26
+
27
+ it('renders checkbox with correct label', () => {
28
+ const { getByText } = render(
29
+ <QualityCheckToggle
30
+ isEditMode={false}
31
+ enabled={true}
32
+ setEnabled={mockSetEnabled}
33
+ />,
34
+ );
35
+
36
+ expect(getByText('Fact-check AI answer')).toBeInTheDocument();
37
+ });
38
+
39
+ it('checkbox is checked when enabled is true', () => {
40
+ const { container } = render(
41
+ <QualityCheckToggle
42
+ isEditMode={false}
43
+ enabled={true}
44
+ setEnabled={mockSetEnabled}
45
+ />,
46
+ );
47
+
48
+ const checkbox = container.querySelector('input[type="checkbox"]');
49
+ expect(checkbox.checked).toBe(true);
50
+ });
51
+
52
+ it('checkbox is unchecked when enabled is false', () => {
53
+ const { container } = render(
54
+ <QualityCheckToggle
55
+ isEditMode={false}
56
+ enabled={false}
57
+ setEnabled={mockSetEnabled}
58
+ />,
59
+ );
60
+
61
+ const checkbox = container.querySelector('input[type="checkbox"]');
62
+ expect(checkbox.checked).toBe(false);
63
+ });
64
+
65
+ it('checkbox is disabled in edit mode', () => {
66
+ const { container } = render(
67
+ <QualityCheckToggle
68
+ isEditMode={true}
69
+ enabled={true}
70
+ setEnabled={mockSetEnabled}
71
+ />,
72
+ );
73
+
74
+ const checkbox = container.querySelector('input[type="checkbox"]');
75
+ expect(checkbox.disabled).toBe(true);
76
+ });
77
+
78
+ it('checkbox is enabled when not in edit mode', () => {
79
+ const { container } = render(
80
+ <QualityCheckToggle
81
+ isEditMode={false}
82
+ enabled={true}
83
+ setEnabled={mockSetEnabled}
84
+ />,
85
+ );
86
+
87
+ const checkbox = container.querySelector('input[type="checkbox"]');
88
+ expect(checkbox.disabled).toBe(false);
89
+ });
90
+
91
+ it('calls setEnabled when checkbox is changed', () => {
92
+ const { container } = render(
93
+ <QualityCheckToggle
94
+ isEditMode={false}
95
+ enabled={true}
96
+ setEnabled={mockSetEnabled}
97
+ />,
98
+ );
99
+
100
+ const checkbox = container.querySelector('input[type="checkbox"]');
101
+ fireEvent.click(checkbox);
102
+
103
+ expect(mockSetEnabled).toHaveBeenCalled();
104
+ });
105
+ });
@@ -0,0 +1,215 @@
1
+ import React from 'react';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import RelatedQuestions from '../components/RelatedQuestions';
5
+
6
+ import { trackEvent } from '@eeacms/volto-matomo/utils';
7
+
8
+ // Mock @eeacms/volto-matomo/utils
9
+ jest.mock('@eeacms/volto-matomo/utils', () => ({
10
+ trackEvent: jest.fn(),
11
+ }));
12
+
13
+ describe('RelatedQuestions', () => {
14
+ const mockOnChoice = jest.fn();
15
+
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ it('renders nothing when no related questions', () => {
21
+ const { container } = render(
22
+ <RelatedQuestions
23
+ message={{ relatedQuestions: [] }}
24
+ onChoice={mockOnChoice}
25
+ isLoading={false}
26
+ />,
27
+ );
28
+ expect(container.querySelector('.chat-related-questions')).toBeNull();
29
+ });
30
+
31
+ it('renders nothing when relatedQuestions is undefined', () => {
32
+ const { container } = render(
33
+ <RelatedQuestions
34
+ message={{}}
35
+ onChoice={mockOnChoice}
36
+ isLoading={false}
37
+ />,
38
+ );
39
+ expect(container.querySelector('.chat-related-questions')).toBeNull();
40
+ });
41
+
42
+ it('renders related questions when available', () => {
43
+ const message = {
44
+ relatedQuestions: [
45
+ { question: 'Question 1' },
46
+ { question: 'Question 2' },
47
+ ],
48
+ };
49
+
50
+ const { getByText } = render(
51
+ <RelatedQuestions
52
+ message={message}
53
+ onChoice={mockOnChoice}
54
+ isLoading={false}
55
+ />,
56
+ );
57
+
58
+ expect(getByText('Related questions:')).toBeInTheDocument();
59
+ expect(getByText('Question 1')).toBeInTheDocument();
60
+ expect(getByText('Question 2')).toBeInTheDocument();
61
+ });
62
+
63
+ it('calls onChoice when question is clicked', () => {
64
+ const message = {
65
+ relatedQuestions: [{ question: 'Test Question' }],
66
+ };
67
+
68
+ const { getByText } = render(
69
+ <RelatedQuestions
70
+ message={message}
71
+ onChoice={mockOnChoice}
72
+ isLoading={false}
73
+ />,
74
+ );
75
+
76
+ fireEvent.click(getByText('Test Question'));
77
+ expect(mockOnChoice).toHaveBeenCalledWith('Test Question');
78
+ });
79
+
80
+ it('does not call onChoice when loading', () => {
81
+ const message = {
82
+ relatedQuestions: [{ question: 'Test Question' }],
83
+ };
84
+
85
+ const { getByText } = render(
86
+ <RelatedQuestions
87
+ message={message}
88
+ onChoice={mockOnChoice}
89
+ isLoading={true}
90
+ />,
91
+ );
92
+
93
+ fireEvent.click(getByText('Test Question'));
94
+ expect(mockOnChoice).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it('tracks event when enableMatomoTracking is true', () => {
98
+ const message = {
99
+ relatedQuestions: [{ question: 'Tracked Question' }],
100
+ };
101
+ const persona = { name: 'Test Persona' };
102
+
103
+ const { getByText } = render(
104
+ <RelatedQuestions
105
+ message={message}
106
+ onChoice={mockOnChoice}
107
+ isLoading={false}
108
+ enableMatomoTracking={true}
109
+ persona={persona}
110
+ />,
111
+ );
112
+
113
+ fireEvent.click(getByText('Tracked Question'));
114
+
115
+ expect(trackEvent).toHaveBeenCalledWith({
116
+ category: 'Chatbot - Test Persona',
117
+ action: 'Chatbot: Related question click',
118
+ name: 'Message submitted',
119
+ });
120
+ });
121
+
122
+ it('tracks event with default category when no persona name', () => {
123
+ const message = {
124
+ relatedQuestions: [{ question: 'Tracked Question' }],
125
+ };
126
+
127
+ const { getByText } = render(
128
+ <RelatedQuestions
129
+ message={message}
130
+ onChoice={mockOnChoice}
131
+ isLoading={false}
132
+ enableMatomoTracking={true}
133
+ persona={{}}
134
+ />,
135
+ );
136
+
137
+ fireEvent.click(getByText('Tracked Question'));
138
+
139
+ expect(trackEvent).toHaveBeenCalledWith({
140
+ category: 'Chatbot',
141
+ action: 'Chatbot: Related question click',
142
+ name: 'Message submitted',
143
+ });
144
+ });
145
+
146
+ it('does not track event when enableMatomoTracking is false', () => {
147
+ const message = {
148
+ relatedQuestions: [{ question: 'Untracked Question' }],
149
+ };
150
+
151
+ const { getByText } = render(
152
+ <RelatedQuestions
153
+ message={message}
154
+ onChoice={mockOnChoice}
155
+ isLoading={false}
156
+ enableMatomoTracking={false}
157
+ />,
158
+ );
159
+
160
+ fireEvent.click(getByText('Untracked Question'));
161
+
162
+ expect(trackEvent).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it('handles keyboard Enter key', () => {
166
+ const message = {
167
+ relatedQuestions: [{ question: 'Keyboard Question' }],
168
+ };
169
+
170
+ const { getByText } = render(
171
+ <RelatedQuestions
172
+ message={message}
173
+ onChoice={mockOnChoice}
174
+ isLoading={false}
175
+ />,
176
+ );
177
+
178
+ fireEvent.keyDown(getByText('Keyboard Question'), { key: 'Enter' });
179
+ expect(mockOnChoice).toHaveBeenCalledWith('Keyboard Question');
180
+ });
181
+
182
+ it('handles keyboard Space key', () => {
183
+ const message = {
184
+ relatedQuestions: [{ question: 'Space Question' }],
185
+ };
186
+
187
+ const { getByText } = render(
188
+ <RelatedQuestions
189
+ message={message}
190
+ onChoice={mockOnChoice}
191
+ isLoading={false}
192
+ />,
193
+ );
194
+
195
+ fireEvent.keyDown(getByText('Space Question'), { key: ' ' });
196
+ expect(mockOnChoice).toHaveBeenCalledWith('Space Question');
197
+ });
198
+
199
+ it('ignores other keyboard keys', () => {
200
+ const message = {
201
+ relatedQuestions: [{ question: 'Other Key Question' }],
202
+ };
203
+
204
+ const { getByText } = render(
205
+ <RelatedQuestions
206
+ message={message}
207
+ onChoice={mockOnChoice}
208
+ isLoading={false}
209
+ />,
210
+ );
211
+
212
+ fireEvent.keyDown(getByText('Other Key Question'), { key: 'Tab' });
213
+ expect(mockOnChoice).not.toHaveBeenCalled();
214
+ });
215
+ });
@@ -0,0 +1,79 @@
1
+ import { MemoryRouter } from 'react-router-dom';
2
+ import configureStore from 'redux-mock-store';
3
+ import renderer from 'react-test-renderer';
4
+
5
+ import '@testing-library/jest-dom/extend-expect';
6
+ import { Provider } from 'react-intl-redux';
7
+ import SourceDetails from '../components/Source';
8
+
9
+ const mockStore = configureStore();
10
+
11
+ jest.mock('@plone/volto/helpers/Loadable/Loadable', () => ({
12
+ injectLazyLibs: () => (Component) => (props) => (
13
+ <Component {...props} luxon={require('luxon')} />
14
+ ),
15
+ }));
16
+
17
+ describe('SourceDetails', () => {
18
+ it('should render the component with link type', () => {
19
+ const store = mockStore({
20
+ userSession: { token: '1234' },
21
+ intl: {
22
+ locale: 'en',
23
+ messages: {},
24
+ },
25
+ });
26
+
27
+ const props = {
28
+ index: '1',
29
+ source: {
30
+ blurb: 'Vestibulum purus quam scelerisque ut',
31
+ link: 'https://www.example.com',
32
+ source_type: 'web',
33
+ semantic_identifier: 'Nam at tortor in tellus',
34
+ updated_at: null,
35
+ },
36
+ };
37
+
38
+ const component = renderer.create(
39
+ <Provider store={store}>
40
+ <MemoryRouter>
41
+ <SourceDetails {...props} />
42
+ </MemoryRouter>
43
+ </Provider>,
44
+ );
45
+ const json = component.toJSON();
46
+ expect(json).toMatchSnapshot();
47
+ });
48
+
49
+ it('should render the component with doc type', () => {
50
+ const store = mockStore({
51
+ userSession: { token: '1234' },
52
+ intl: {
53
+ locale: 'en',
54
+ messages: {},
55
+ },
56
+ });
57
+
58
+ const props = {
59
+ index: '2',
60
+ source: {
61
+ blurb: 'Vestibulum purus quam scelerisque ut',
62
+ link: 'https://www.example.com',
63
+ source_type: 'file',
64
+ semantic_identifier: 'Nam at tortor in tellus',
65
+ updated_at: null,
66
+ },
67
+ };
68
+
69
+ const component = renderer.create(
70
+ <Provider store={store}>
71
+ <MemoryRouter>
72
+ <SourceDetails {...props} />
73
+ </MemoryRouter>
74
+ </Provider>,
75
+ );
76
+ const json = component.toJSON();
77
+ expect(json).toMatchSnapshot();
78
+ });
79
+ });
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import renderer from 'react-test-renderer';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import Spinner from '../components/Spinner';
5
+
6
+ describe('Spinner', () => {
7
+ it('renders correctly', () => {
8
+ const component = renderer.create(<Spinner />);
9
+ expect(component.toJSON()).toMatchSnapshot();
10
+ });
11
+
12
+ it('renders a div with spinner class', () => {
13
+ const component = renderer.create(<Spinner />);
14
+ const tree = component.toJSON();
15
+ expect(tree.type).toBe('div');
16
+ expect(tree.props.className).toBe('spinner');
17
+ });
18
+ });
@@ -0,0 +1,51 @@
1
+ import '@testing-library/jest-dom/extend-expect';
2
+
3
+ import installChatBlock from '../index';
4
+
5
+ // Mock @plone/volto/components
6
+ jest.mock('@plone/volto/components', () => ({
7
+ SidebarPortal: () => null,
8
+ }));
9
+
10
+ // Mock other dependencies
11
+ jest.mock('../ChatBlockView', () => () => <div>ChatBlockView</div>);
12
+ jest.mock('../ChatBlockEdit', () => () => <div>ChatBlockEdit</div>);
13
+
14
+ describe('ChatBlock installation', () => {
15
+ let mockConfig;
16
+
17
+ beforeEach(() => {
18
+ mockConfig = {
19
+ blocks: {
20
+ blocksConfig: {},
21
+ },
22
+ settings: {},
23
+ };
24
+ });
25
+
26
+ it('should register eeaChatbot block', () => {
27
+ installChatBlock(mockConfig);
28
+
29
+ expect(mockConfig.blocks.blocksConfig.eeaChatbot).toBeDefined();
30
+ });
31
+
32
+ it('should set correct block properties', () => {
33
+ installChatBlock(mockConfig);
34
+
35
+ const blockConfig = mockConfig.blocks.blocksConfig.eeaChatbot;
36
+ expect(blockConfig.id).toBe('eeaChatbot');
37
+ expect(blockConfig.title).toBe('AI Chatbot');
38
+ expect(blockConfig.group).toBe('common');
39
+ expect(blockConfig.restricted({ user: null })).toBe(false);
40
+ expect(blockConfig.mostUsed).toBe(false);
41
+ expect(blockConfig.sidebarTab).toBe(1);
42
+ });
43
+
44
+ it('should set view and edit components', () => {
45
+ installChatBlock(mockConfig);
46
+
47
+ const blockConfig = mockConfig.blocks.blocksConfig.eeaChatbot;
48
+ expect(blockConfig.view).toBeDefined();
49
+ expect(blockConfig.edit).toBeDefined();
50
+ });
51
+ });