@eeacms/volto-eea-chatbot 2.0.0 → 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 (81) hide show
  1. package/.eslintrc.js +6 -6
  2. package/CHANGELOG.md +12 -1
  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/index.js +8 -0
  25. package/src/ChatBlock/packets/MultiToolRenderer.tsx +11 -12
  26. package/src/ChatBlock/packets/RendererComponent.tsx +6 -3
  27. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +3 -3
  28. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +3 -3
  29. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +3 -3
  30. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -8
  31. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +5 -5
  32. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +10 -10
  33. package/src/ChatBlock/services/messageProcessor.ts +6 -3
  34. package/src/ChatBlock/services/packetUtils.ts +2 -2
  35. package/src/ChatBlock/services/streamingService.ts +8 -2
  36. package/src/ChatBlock/utils/citations.ts +1 -1
  37. package/src/halloumi/filtering.test.js +199 -1
  38. package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
  39. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
  40. package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
  41. package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
  42. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
  43. package/src/ChatBlock/tests/Citation.test.jsx +0 -107
  44. package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
  45. package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
  46. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
  47. package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
  48. package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
  49. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
  50. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
  51. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
  52. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
  53. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
  54. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
  55. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
  56. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
  57. package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
  58. package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
  59. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
  60. package/src/ChatBlock/tests/Source.test.jsx +0 -79
  61. package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
  62. package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
  63. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
  64. package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
  65. package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
  66. package/src/ChatBlock/tests/citations.test.js +0 -114
  67. package/src/ChatBlock/tests/index.test.js +0 -51
  68. package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
  69. package/src/ChatBlock/tests/packetUtils.test.js +0 -158
  70. package/src/ChatBlock/tests/schema.test.js +0 -166
  71. package/src/ChatBlock/tests/streamingService.test.js +0 -467
  72. package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
  73. package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
  74. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
  75. package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
  76. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
  77. package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
  78. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
  79. package/src/ChatBlock/tests/utils.test.jsx +0 -241
  80. package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
  81. /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
- });