@eeacms/volto-eea-chatbot 2.0.1 → 2.0.2

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 (80) hide show
  1. package/.eslintrc.js +6 -6
  2. package/CHANGELOG.md +2 -0
  3. package/jest-addon.config.js +1 -0
  4. package/package.json +1 -1
  5. package/src/ChatBlock/ChatBlockEdit.jsx +2 -1
  6. package/src/ChatBlock/chat/AIMessage.tsx +20 -16
  7. package/src/ChatBlock/chat/ChatMessage.tsx +1 -1
  8. package/src/ChatBlock/chat/ChatWindow.tsx +10 -11
  9. package/src/ChatBlock/chat/UserMessage.tsx +4 -4
  10. package/src/ChatBlock/components/AutoResizeTextarea.jsx +1 -1
  11. package/src/ChatBlock/components/ChatMessageFeedback.jsx +2 -2
  12. package/src/ChatBlock/components/EmptyState.jsx +1 -1
  13. package/src/ChatBlock/components/FeedbackModal.jsx +1 -1
  14. package/src/ChatBlock/components/HalloumiFeedback.jsx +2 -2
  15. package/src/ChatBlock/components/Source.jsx +2 -2
  16. package/src/ChatBlock/components/UserActionsToolbar.jsx +3 -3
  17. package/src/ChatBlock/components/WebResultIcon.tsx +2 -2
  18. package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
  19. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +4 -4
  20. package/src/ChatBlock/components/markdown/{index.js → index.jsx} +1 -1
  21. package/src/ChatBlock/hooks/useChatController.ts +7 -4
  22. package/src/ChatBlock/hooks/useChatStreaming.ts +4 -4
  23. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +1 -1
  24. package/src/ChatBlock/packets/MultiToolRenderer.tsx +11 -12
  25. package/src/ChatBlock/packets/RendererComponent.tsx +6 -3
  26. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +3 -3
  27. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +3 -3
  28. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +3 -3
  29. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -8
  30. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +5 -5
  31. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +10 -10
  32. package/src/ChatBlock/services/messageProcessor.ts +6 -3
  33. package/src/ChatBlock/services/packetUtils.ts +2 -2
  34. package/src/ChatBlock/services/streamingService.ts +8 -2
  35. package/src/ChatBlock/utils/citations.ts +1 -1
  36. package/src/halloumi/filtering.test.js +199 -1
  37. package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
  38. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
  39. package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
  40. package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
  41. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
  42. package/src/ChatBlock/tests/Citation.test.jsx +0 -107
  43. package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
  44. package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
  45. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
  46. package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
  47. package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
  48. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
  49. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
  50. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
  51. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
  52. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
  53. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
  54. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
  55. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
  56. package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
  57. package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
  58. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
  59. package/src/ChatBlock/tests/Source.test.jsx +0 -79
  60. package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
  61. package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
  62. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
  63. package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
  64. package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
  65. package/src/ChatBlock/tests/citations.test.js +0 -114
  66. package/src/ChatBlock/tests/index.test.js +0 -51
  67. package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
  68. package/src/ChatBlock/tests/packetUtils.test.js +0 -158
  69. package/src/ChatBlock/tests/schema.test.js +0 -166
  70. package/src/ChatBlock/tests/streamingService.test.js +0 -467
  71. package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
  72. package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
  73. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
  74. package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
  75. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
  76. package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
  77. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
  78. package/src/ChatBlock/tests/utils.test.jsx +0 -241
  79. package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
  80. /package/src/ChatBlock/{schema.js → schema.jsx} +0 -0
@@ -1,150 +0,0 @@
1
- import { renderHook, act } from '@testing-library/react-hooks';
2
-
3
- import { useQualityMarkers } from '../hooks/useQualityMarkers';
4
-
5
- // Mock loadable before importing the hook
6
- jest.mock('@loadable/component', () => {
7
- const loadable = () => {
8
- const MockComponent = ({ children }) => null;
9
- return MockComponent;
10
- };
11
- loadable.lib = () => {
12
- const MockLibComponent = () => null;
13
- MockLibComponent.load = () =>
14
- Promise.resolve({ captureException: jest.fn() });
15
- return MockLibComponent;
16
- };
17
- return { __esModule: true, default: loadable };
18
- });
19
-
20
- describe('useQualityMarkers', () => {
21
- beforeEach(() => {
22
- global.fetch = jest.fn();
23
- });
24
-
25
- afterEach(() => {
26
- jest.restoreAllMocks();
27
- });
28
-
29
- it('returns null markers when doQualityControl is false', () => {
30
- const { result } = renderHook(() =>
31
- useQualityMarkers(false, 'test message', []),
32
- );
33
-
34
- expect(result.current.markers).toBeNull();
35
- expect(result.current.isLoadingHalloumi).toBe(false);
36
- });
37
-
38
- it('returns failure rationale when sources are empty', async () => {
39
- let hookResult;
40
- await act(async () => {
41
- const { result } = renderHook(() =>
42
- useQualityMarkers(true, 'test message', []),
43
- );
44
- // Wait a tick for the async useEffect handler to complete
45
- await new Promise((r) => setTimeout(r, 0));
46
- hookResult = result;
47
- });
48
-
49
- expect(hookResult.current.markers).toBeDefined();
50
- expect(hookResult.current.markers.claims[0].rationale).toBe(
51
- 'Answer cannot be verified due to empty sources.',
52
- );
53
- });
54
-
55
- it('fetches halloumi response when sources are provided', async () => {
56
- global.fetch.mockResolvedValue({
57
- json: () =>
58
- Promise.resolve({
59
- claims: [
60
- {
61
- startOffset: 0,
62
- endOffset: 12,
63
- score: 0.9,
64
- rationale: 'Supported',
65
- },
66
- ],
67
- segments: {},
68
- }),
69
- });
70
-
71
- const sources = [{ halloumiContext: 'source text', id: '1' }];
72
-
73
- const { result, waitForValueToChange } = renderHook(() =>
74
- useQualityMarkers(true, 'test message', sources),
75
- );
76
-
77
- await waitForValueToChange(() => result.current.markers);
78
-
79
- expect(result.current.markers).toBeDefined();
80
- expect(result.current.markers.claims).toHaveLength(1);
81
- expect(result.current.isLoadingHalloumi).toBe(false);
82
- });
83
-
84
- it('retryHalloumi resets response', async () => {
85
- global.fetch.mockResolvedValue({
86
- json: () =>
87
- Promise.resolve({
88
- claims: [
89
- { startOffset: 0, endOffset: 12, score: 0.9, rationale: 'Ok' },
90
- ],
91
- segments: {},
92
- }),
93
- });
94
-
95
- const sources = [{ halloumiContext: 'source text', id: '1' }];
96
-
97
- const { result, waitForValueToChange } = renderHook(() =>
98
- useQualityMarkers(true, 'test message', sources),
99
- );
100
-
101
- await waitForValueToChange(() => result.current.markers);
102
- expect(result.current.markers).not.toBeNull();
103
-
104
- act(() => {
105
- result.current.retryHalloumi();
106
- });
107
-
108
- expect(result.current.markers).toBeNull();
109
- });
110
-
111
- it('filters out claims with special characters and low score', async () => {
112
- global.fetch.mockResolvedValue({
113
- json: () =>
114
- Promise.resolve({
115
- claims: [
116
- {
117
- startOffset: 0,
118
- endOffset: 1,
119
- score: 0.01,
120
- rationale: 'Low',
121
- },
122
- {
123
- startOffset: 0,
124
- endOffset: 12,
125
- score: 0.9,
126
- rationale: 'Ok',
127
- },
128
- ],
129
- segments: {},
130
- }),
131
- });
132
-
133
- const sources = [{ halloumiContext: 'source text', id: '1' }];
134
- const message = '* test message';
135
-
136
- const { result, waitForValueToChange } = renderHook(() =>
137
- useQualityMarkers(true, message, sources),
138
- );
139
-
140
- await waitForValueToChange(() => result.current.markers);
141
- // Claims with special chars only and small score get filtered
142
- expect(result.current.markers.claims.length).toBeGreaterThanOrEqual(1);
143
- });
144
-
145
- it('provides retryHalloumi callback', () => {
146
- const { result } = renderHook(() => useQualityMarkers(false, 'test', []));
147
-
148
- expect(typeof result.current.retryHalloumi).toBe('function');
149
- });
150
- });
@@ -1,121 +0,0 @@
1
- import { renderHook, act } from '@testing-library/react-hooks';
2
- import { useScrollonStream } from '../hooks/useScrollonStream';
3
-
4
- describe('useScrollonStream', () => {
5
- let addEventListenerSpy;
6
- let removeEventListenerSpy;
7
-
8
- beforeEach(() => {
9
- jest.useFakeTimers();
10
- addEventListenerSpy = jest.spyOn(window, 'addEventListener');
11
- removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
12
- });
13
-
14
- afterEach(() => {
15
- jest.useRealTimers();
16
- addEventListenerSpy.mockRestore();
17
- removeEventListenerSpy.mockRestore();
18
- });
19
-
20
- it('sets up event listeners when enabled and streaming', () => {
21
- const bottomRef = { current: document.createElement('div') };
22
- renderHook(() =>
23
- useScrollonStream({
24
- bottomRef,
25
- isStreaming: true,
26
- enabled: true,
27
- }),
28
- );
29
-
30
- const eventTypes = addEventListenerSpy.mock.calls.map((c) => c[0]);
31
- expect(eventTypes).toContain('wheel');
32
- expect(eventTypes).toContain('touchstart');
33
- expect(eventTypes).toContain('keydown');
34
- expect(eventTypes).toContain('mousedown');
35
- });
36
-
37
- it('disables scrolling on user wheel event', () => {
38
- const bottomRef = { current: document.createElement('div') };
39
- renderHook(() =>
40
- useScrollonStream({
41
- bottomRef,
42
- isStreaming: true,
43
- enabled: true,
44
- }),
45
- );
46
-
47
- act(() => {
48
- window.dispatchEvent(new Event('wheel'));
49
- });
50
-
51
- // After wheel event, the scroll listener should be removed
52
- expect(removeEventListenerSpy).toHaveBeenCalled();
53
- });
54
-
55
- it('sets isActive to false after streaming stops with grace period', () => {
56
- const bottomRef = { current: document.createElement('div') };
57
- const { rerender } = renderHook(
58
- ({ isStreaming }) =>
59
- useScrollonStream({
60
- bottomRef,
61
- isStreaming,
62
- enabled: true,
63
- }),
64
- { initialProps: { isStreaming: true } },
65
- );
66
-
67
- // Stop streaming
68
- rerender({ isStreaming: false });
69
-
70
- // Should still be active before timeout
71
- act(() => {
72
- jest.advanceTimersByTime(500);
73
- });
74
- });
75
-
76
- it('does not set up event listeners when disabled', () => {
77
- const bottomRef = { current: document.createElement('div') };
78
- const callsBefore = addEventListenerSpy.mock.calls.length;
79
-
80
- renderHook(() =>
81
- useScrollonStream({
82
- bottomRef,
83
- isStreaming: true,
84
- enabled: false,
85
- }),
86
- );
87
-
88
- // Only the streaming state listener calls, not user input listeners
89
- const newCalls = addEventListenerSpy.mock.calls
90
- .slice(callsBefore)
91
- .map((c) => c[0]);
92
- expect(newCalls).not.toContain('wheel');
93
- });
94
-
95
- it('handles missing bottomRef gracefully', () => {
96
- const bottomRef = { current: null };
97
- expect(() => {
98
- renderHook(() =>
99
- useScrollonStream({
100
- bottomRef,
101
- isStreaming: true,
102
- enabled: true,
103
- }),
104
- );
105
- }).not.toThrow();
106
- });
107
-
108
- it('cleans up on unmount', () => {
109
- const bottomRef = { current: document.createElement('div') };
110
- const { unmount } = renderHook(() =>
111
- useScrollonStream({
112
- bottomRef,
113
- isStreaming: true,
114
- enabled: true,
115
- }),
116
- );
117
-
118
- unmount();
119
- expect(removeEventListenerSpy).toHaveBeenCalled();
120
- });
121
- });
@@ -1,151 +0,0 @@
1
- import { renderHook, act } from '@testing-library/react-hooks';
2
- import { useToolDisplayTiming } from '../hooks/useToolDisplayTiming';
3
-
4
- describe('useToolDisplayTiming', () => {
5
- beforeEach(() => {
6
- jest.useFakeTimers();
7
- });
8
-
9
- afterEach(() => {
10
- jest.useRealTimers();
11
- });
12
-
13
- it('should initialize with all tools visible when not complete', () => {
14
- const toolGroups = [
15
- { ind: 1, packets: [] },
16
- { ind: 2, packets: [] },
17
- ];
18
-
19
- const { result } = renderHook(() =>
20
- useToolDisplayTiming(toolGroups, false, false),
21
- );
22
-
23
- expect(result.current.visibleTools).toEqual(new Set([1, 2]));
24
- expect(result.current.allToolsDisplayed).toBe(false);
25
- });
26
-
27
- it('should initialize with all tools visible when complete', () => {
28
- const toolGroups = [
29
- { ind: 1, packets: [] },
30
- { ind: 2, packets: [] },
31
- ];
32
-
33
- const { result } = renderHook(() =>
34
- useToolDisplayTiming(toolGroups, false, true),
35
- );
36
-
37
- expect(result.current.visibleTools).toEqual(new Set([1, 2]));
38
- expect(result.current.allToolsDisplayed).toBe(false);
39
- });
40
-
41
- it('should show first tool immediately', () => {
42
- const toolGroups = [{ ind: 1, packets: [] }];
43
-
44
- const { result } = renderHook(() =>
45
- useToolDisplayTiming(toolGroups, false, false),
46
- );
47
-
48
- expect(result.current.visibleTools.has(1)).toBe(true);
49
- });
50
-
51
- it('should handle tool completion without errors', () => {
52
- const toolGroups = [
53
- { ind: 1, packets: [] },
54
- { ind: 2, packets: [] },
55
- ];
56
-
57
- const { result } = renderHook(() =>
58
- useToolDisplayTiming(toolGroups, false, false),
59
- );
60
-
61
- // All tools should be visible
62
- expect(result.current.visibleTools.has(1)).toBe(true);
63
- expect(result.current.visibleTools.has(2)).toBe(true);
64
-
65
- // Complete first tool - should not throw errors
66
- expect(() => {
67
- act(() => {
68
- result.current.handleToolComplete(1);
69
- });
70
- }).not.toThrow();
71
- });
72
-
73
- it('should handle tool completion with timeout', () => {
74
- const toolGroups = [{ ind: 1, packets: [] }];
75
-
76
- const { result } = renderHook(() =>
77
- useToolDisplayTiming(toolGroups, false, false),
78
- );
79
-
80
- // Complete tool - should schedule timeout if not enough time passed
81
- act(() => {
82
- result.current.handleToolComplete(1);
83
- });
84
-
85
- // Should not throw and tool should still be visible
86
- expect(result.current.visibleTools.has(1)).toBe(true);
87
- });
88
-
89
- it('should mark all tools as displayed when complete and final message coming', () => {
90
- const toolGroups = [{ ind: 1, packets: [] }];
91
-
92
- const { result } = renderHook(() =>
93
- useToolDisplayTiming(toolGroups, true, true),
94
- );
95
-
96
- // Mark tool as completed
97
- act(() => {
98
- result.current.handleToolComplete(1);
99
- });
100
-
101
- expect(result.current.allToolsDisplayed).toBe(true);
102
- });
103
-
104
- it('should not mark all tools as displayed when final message not coming', () => {
105
- const toolGroups = [{ ind: 1, packets: [] }];
106
-
107
- const { result } = renderHook(() =>
108
- useToolDisplayTiming(toolGroups, false, true),
109
- );
110
-
111
- expect(result.current.allToolsDisplayed).toBe(false);
112
- });
113
-
114
- it('should handle empty tool groups', () => {
115
- const { result } = renderHook(() => useToolDisplayTiming([], false, false));
116
-
117
- expect(result.current.visibleTools).toEqual(new Set());
118
- expect(result.current.allToolsDisplayed).toBe(true);
119
- });
120
-
121
- it('should not complete tool twice', () => {
122
- const toolGroups = [{ ind: 1, packets: [] }];
123
-
124
- const { result } = renderHook(() =>
125
- useToolDisplayTiming(toolGroups, false, false),
126
- );
127
-
128
- act(() => {
129
- result.current.handleToolComplete(1);
130
- result.current.handleToolComplete(1);
131
- });
132
-
133
- // Should still work without errors
134
- expect(result.current.visibleTools.has(1)).toBe(true);
135
- });
136
-
137
- it('should clean up timeouts on unmount', () => {
138
- const toolGroups = [{ ind: 1, packets: [] }];
139
-
140
- const { result, unmount } = renderHook(() =>
141
- useToolDisplayTiming(toolGroups, false, false),
142
- );
143
-
144
- act(() => {
145
- result.current.handleToolComplete(1);
146
- });
147
-
148
- // Unmount should not throw errors
149
- expect(() => unmount()).not.toThrow();
150
- });
151
- });
@@ -1,241 +0,0 @@
1
- import { act } from '@testing-library/react';
2
- import { renderHook } from '@testing-library/react-hooks';
3
- import '@testing-library/jest-dom';
4
- import {
5
- EMAIL_REGEX,
6
- transformEmailsToLinks,
7
- debounce,
8
- useCopyToClipboard,
9
- convertToPercentage,
10
- createChatMessageFeedback,
11
- } from '../utils';
12
-
13
- describe('utils', () => {
14
- describe('EMAIL_REGEX', () => {
15
- it('matches valid email addresses', () => {
16
- expect('test@example.com').toMatch(EMAIL_REGEX);
17
- expect('user.name@domain.org').toMatch(EMAIL_REGEX);
18
- expect('user+tag@example.co.uk').toMatch(EMAIL_REGEX);
19
- });
20
-
21
- it('does not match invalid email addresses', () => {
22
- expect('not-an-email').not.toMatch(EMAIL_REGEX);
23
- expect('@missing-local.com').not.toMatch(EMAIL_REGEX);
24
- });
25
- });
26
-
27
- describe('transformEmailsToLinks', () => {
28
- it('transforms email addresses to mailto links', () => {
29
- const text = 'Contact us at test@example.com';
30
- const result = transformEmailsToLinks(text);
31
- // Result includes: ['Contact us at ', <a>email</a>, '']
32
- expect(result.length).toBeGreaterThanOrEqual(2);
33
- expect(result[0]).toBe('Contact us at ');
34
- });
35
-
36
- it('handles text without email addresses', () => {
37
- const text = 'No email here';
38
- const result = transformEmailsToLinks(text);
39
- expect(result).toHaveLength(1);
40
- expect(result[0]).toBe('No email here');
41
- });
42
-
43
- it('handles multiple email addresses', () => {
44
- const text = 'Email one@test.com and two@test.com';
45
- const result = transformEmailsToLinks(text);
46
- expect(result.length).toBeGreaterThan(1);
47
- });
48
- });
49
-
50
- describe('debounce', () => {
51
- beforeEach(() => {
52
- jest.useFakeTimers();
53
- });
54
-
55
- afterEach(() => {
56
- jest.useRealTimers();
57
- });
58
-
59
- it('calls the function immediately on first call', () => {
60
- const callable = jest.fn();
61
- const clickSignal = { current: null };
62
-
63
- debounce(callable, clickSignal);
64
-
65
- expect(callable).toHaveBeenCalledTimes(1);
66
- });
67
-
68
- it('prevents multiple calls within debounce period', () => {
69
- const callable = jest.fn();
70
- const clickSignal = { current: null };
71
-
72
- debounce(callable, clickSignal);
73
- debounce(callable, clickSignal);
74
- debounce(callable, clickSignal);
75
-
76
- expect(callable).toHaveBeenCalledTimes(1);
77
- });
78
-
79
- it('allows call after debounce period', () => {
80
- const callable = jest.fn();
81
- const clickSignal = { current: null };
82
-
83
- debounce(callable, clickSignal);
84
- expect(callable).toHaveBeenCalledTimes(1);
85
-
86
- jest.advanceTimersByTime(1000);
87
-
88
- debounce(callable, clickSignal);
89
- expect(callable).toHaveBeenCalledTimes(2);
90
- });
91
- });
92
-
93
- describe('useCopyToClipboard', () => {
94
- beforeEach(() => {
95
- jest.useFakeTimers();
96
- Object.assign(navigator, {
97
- clipboard: {
98
- writeText: jest.fn().mockResolvedValue(undefined),
99
- },
100
- });
101
- });
102
-
103
- afterEach(() => {
104
- jest.useRealTimers();
105
- });
106
-
107
- it('returns initial state as not copied', () => {
108
- const { result } = renderHook(() => useCopyToClipboard('test'));
109
- expect(result.current[0]).toBe(false);
110
- });
111
-
112
- it('copies text to clipboard when copy is called', async () => {
113
- const { result } = renderHook(() => useCopyToClipboard('test text'));
114
-
115
- await act(async () => {
116
- result.current[1]();
117
- });
118
-
119
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test text');
120
- expect(result.current[0]).toBe(true);
121
- });
122
-
123
- it('resets copied state after timeout', async () => {
124
- const { result } = renderHook(() => useCopyToClipboard('test'));
125
-
126
- await act(async () => {
127
- result.current[1]();
128
- });
129
-
130
- expect(result.current[0]).toBe(true);
131
-
132
- act(() => {
133
- jest.advanceTimersByTime(2000);
134
- });
135
-
136
- expect(result.current[0]).toBe(false);
137
- });
138
-
139
- it('handles clipboard write failure', async () => {
140
- navigator.clipboard.writeText = jest.fn().mockRejectedValue(new Error());
141
-
142
- const { result } = renderHook(() => useCopyToClipboard('test'));
143
-
144
- await act(async () => {
145
- result.current[1]();
146
- });
147
-
148
- expect(result.current[0]).toBe(false);
149
- });
150
- });
151
-
152
- describe('convertToPercentage', () => {
153
- it('converts float to percentage string', () => {
154
- expect(convertToPercentage(0.5)).toBe('50.00%');
155
- expect(convertToPercentage(0.123)).toBe('12.30%');
156
- expect(convertToPercentage(1)).toBe('100.00%');
157
- expect(convertToPercentage(0)).toBe('0.00%');
158
- });
159
-
160
- it('handles custom digit precision', () => {
161
- expect(convertToPercentage(0.5, 0)).toBe('50%');
162
- expect(convertToPercentage(0.5, 1)).toBe('50.0%');
163
- expect(convertToPercentage(0.123, 1)).toBe('12.3%');
164
- });
165
-
166
- it('returns 0% for values outside 0-1 range', () => {
167
- expect(convertToPercentage(-0.5)).toBe('0%');
168
- expect(convertToPercentage(1.5)).toBe('0%');
169
- expect(convertToPercentage(-1)).toBe('0%');
170
- });
171
- });
172
-
173
- describe('createChatMessageFeedback', () => {
174
- beforeEach(() => {
175
- global.fetch = jest.fn();
176
- });
177
-
178
- afterEach(() => {
179
- jest.resetAllMocks();
180
- });
181
-
182
- it('sends positive feedback correctly', async () => {
183
- const mockResponse = { success: true };
184
- global.fetch.mockResolvedValue({
185
- ok: true,
186
- json: () => Promise.resolve(mockResponse),
187
- });
188
-
189
- await createChatMessageFeedback({
190
- chat_message_id: '123',
191
- is_positive: true,
192
- feedback_text: 'Great response!',
193
- });
194
-
195
- expect(fetch).toHaveBeenCalledWith(
196
- '/_da/chat/create-chat-message-feedback',
197
- expect.objectContaining({
198
- method: 'POST',
199
- headers: { 'Content-Type': 'application/json' },
200
- }),
201
- );
202
-
203
- const callBody = JSON.parse(fetch.mock.calls[0][1].body);
204
- expect(callBody.chat_message_id).toBe('123');
205
- expect(callBody.is_positive).toBe(true);
206
- expect(callBody.predefined_feedback).toBeUndefined();
207
- });
208
-
209
- it('sends negative feedback with predefined reason', async () => {
210
- const mockResponse = { success: true };
211
- global.fetch.mockResolvedValue({
212
- ok: true,
213
- json: () => Promise.resolve(mockResponse),
214
- });
215
-
216
- await createChatMessageFeedback({
217
- chat_message_id: '123',
218
- is_positive: false,
219
- predefined_feedback: 'Incorrect information',
220
- });
221
-
222
- const callBody = JSON.parse(fetch.mock.calls[0][1].body);
223
- expect(callBody.is_positive).toBe(false);
224
- expect(callBody.predefined_feedback).toBe('Incorrect information');
225
- });
226
-
227
- it('throws error on failed request', async () => {
228
- global.fetch.mockResolvedValue({
229
- ok: false,
230
- status: 500,
231
- });
232
-
233
- await expect(
234
- createChatMessageFeedback({
235
- chat_message_id: '123',
236
- is_positive: true,
237
- }),
238
- ).rejects.toThrow('Failed to submit feedback.');
239
- });
240
- });
241
- });