@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.
- package/.coverage.babel.config.js +9 -0
- package/.eslintrc.js +68 -0
- package/.husky/pre-commit +2 -0
- package/.release-it.json +17 -0
- package/AGENTS.md +89 -0
- package/CHANGELOG.md +770 -0
- package/DEVELOP.md +124 -0
- package/LICENSE.md +9 -0
- package/README.md +170 -0
- package/RELEASE.md +74 -0
- package/TESTING.md +5 -0
- package/babel.config.js +17 -0
- package/bootstrap +41 -0
- package/cypress.config.js +27 -0
- package/docker-compose.yml +32 -0
- package/jest-addon.config.js +465 -0
- package/jest.setup.js +65 -0
- package/locales/de/LC_MESSAGES/volto.po +14 -0
- package/locales/en/LC_MESSAGES/volto.po +14 -0
- package/locales/it/LC_MESSAGES/volto.po +14 -0
- package/locales/ro/LC_MESSAGES/volto.po +14 -0
- package/locales/volto.pot +16 -0
- package/package.json +98 -0
- package/razzle.extend.js +40 -0
- package/src/ChatBlock/ChatBlockEdit.jsx +46 -0
- package/src/ChatBlock/ChatBlockView.jsx +21 -0
- package/src/ChatBlock/chat/AIMessage.tsx +566 -0
- package/src/ChatBlock/chat/ChatMessage.tsx +35 -0
- package/src/ChatBlock/chat/ChatWindow.tsx +288 -0
- package/src/ChatBlock/chat/UserMessage.tsx +27 -0
- package/src/ChatBlock/chat/index.ts +4 -0
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +67 -0
- package/src/ChatBlock/components/BlinkingDot.tsx +3 -0
- package/src/ChatBlock/components/ChatMessageFeedback.jsx +77 -0
- package/src/ChatBlock/components/EmptyState.jsx +70 -0
- package/src/ChatBlock/components/FeedbackModal.jsx +125 -0
- package/src/ChatBlock/components/HalloumiFeedback.jsx +126 -0
- package/src/ChatBlock/components/Icon.tsx +35 -0
- package/src/ChatBlock/components/QualityCheckToggle.jsx +26 -0
- package/src/ChatBlock/components/RelatedQuestions.jsx +59 -0
- package/src/ChatBlock/components/Source.jsx +93 -0
- package/src/ChatBlock/components/SourceChip.tsx +55 -0
- package/src/ChatBlock/components/Spinner.jsx +3 -0
- package/src/ChatBlock/components/UserActionsToolbar.jsx +44 -0
- package/src/ChatBlock/components/WebResultIcon.tsx +42 -0
- package/src/ChatBlock/components/markdown/Citation.jsx +70 -0
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +98 -0
- package/src/ChatBlock/components/markdown/ClaimSegments.jsx +172 -0
- package/src/ChatBlock/components/markdown/RenderClaimView.jsx +96 -0
- package/src/ChatBlock/components/markdown/colors.js +29 -0
- package/src/ChatBlock/components/markdown/colors.less +52 -0
- package/src/ChatBlock/components/markdown/colors.test.js +69 -0
- package/src/ChatBlock/components/markdown/index.js +115 -0
- package/src/ChatBlock/fonts/DejaVuSans.ttf +0 -0
- package/src/ChatBlock/hocs/withOnyxData.jsx +46 -0
- package/src/ChatBlock/hooks/index.ts +7 -0
- package/src/ChatBlock/hooks/useChatController.ts +333 -0
- package/src/ChatBlock/hooks/useChatStreaming.ts +82 -0
- package/src/ChatBlock/hooks/useDeepCompareMemoize.js +17 -0
- package/src/ChatBlock/hooks/useMarked.js +44 -0
- package/src/ChatBlock/hooks/useQualityMarkers.js +119 -0
- package/src/ChatBlock/hooks/useScrollonStream.ts +131 -0
- package/src/ChatBlock/hooks/useToolDisplayTiming.ts +80 -0
- package/src/ChatBlock/index.js +32 -0
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +235 -0
- package/src/ChatBlock/packets/RendererComponent.tsx +115 -0
- package/src/ChatBlock/packets/index.ts +4 -0
- package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +63 -0
- package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +59 -0
- package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +62 -0
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +172 -0
- package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +122 -0
- package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +323 -0
- package/src/ChatBlock/packets/renderers/index.ts +6 -0
- package/src/ChatBlock/schema.js +403 -0
- package/src/ChatBlock/services/index.ts +3 -0
- package/src/ChatBlock/services/messageProcessor.ts +348 -0
- package/src/ChatBlock/services/packetUtils.ts +48 -0
- package/src/ChatBlock/services/streamingService.ts +342 -0
- package/src/ChatBlock/style.less +1881 -0
- package/src/ChatBlock/tests/AIMessage.test.jsx +95 -0
- package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +49 -0
- package/src/ChatBlock/tests/BlinkingDot.test.jsx +71 -0
- package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +73 -0
- package/src/ChatBlock/tests/Citation.test.jsx +107 -0
- package/src/ChatBlock/tests/EmptyState.test.jsx +137 -0
- package/src/ChatBlock/tests/FeedbackModal.test.jsx +138 -0
- package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +94 -0
- package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +105 -0
- package/src/ChatBlock/tests/RelatedQuestions.test.jsx +215 -0
- package/src/ChatBlock/tests/Source.test.jsx +79 -0
- package/src/ChatBlock/tests/Spinner.test.jsx +18 -0
- package/src/ChatBlock/tests/index.test.js +51 -0
- package/src/ChatBlock/tests/messageProcessor.test.jsx +154 -0
- package/src/ChatBlock/tests/schema.test.js +166 -0
- package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +107 -0
- package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +151 -0
- package/src/ChatBlock/types/cssmodules.d.ts +7 -0
- package/src/ChatBlock/types/interfaces.ts +154 -0
- package/src/ChatBlock/types/slate.d.ts +3 -0
- package/src/ChatBlock/types/streamingModels.ts +267 -0
- package/src/ChatBlock/types/volto.d.ts +3 -0
- package/src/ChatBlock/utils/citations.ts +25 -0
- package/src/ChatBlock/utils/index.tsx +114 -0
- package/src/halloumi/README.md +1 -0
- package/src/halloumi/generative.js +219 -0
- package/src/halloumi/generative.test.js +88 -0
- package/src/halloumi/middleware.js +70 -0
- package/src/halloumi/postprocessing.js +273 -0
- package/src/halloumi/postprocessing.test.js +441 -0
- package/src/halloumi/preprocessing.js +115 -0
- package/src/halloumi/preprocessing.test.js +245 -0
- package/src/icons/bot.svg +1 -0
- package/src/icons/check.svg +1 -0
- package/src/icons/chevron.svg +3 -0
- package/src/icons/clear.svg +1 -0
- package/src/icons/copy.svg +1 -0
- package/src/icons/done.svg +5 -0
- package/src/icons/external-link.svg +1 -0
- package/src/icons/file.svg +1 -0
- package/src/icons/glasses.svg +1 -0
- package/src/icons/globe.svg +1 -0
- package/src/icons/rotate.svg +1 -0
- package/src/icons/search.svg +5 -0
- package/src/icons/send.svg +1 -0
- package/src/icons/square-pen.svg +1 -0
- package/src/icons/stop.svg +9 -0
- package/src/icons/thumbs-down.svg +1 -0
- package/src/icons/thumbs-up.svg +1 -0
- package/src/icons/user.svg +1 -0
- package/src/index.js +58 -0
- package/src/middleware.js +250 -0
- 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
|
+
});
|