@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,154 @@
1
+ import { MessageProcessor } from '../services/messageProcessor';
2
+ import { PacketType } from '../types/streamingModels';
3
+
4
+ describe('MessageProcessor', () => {
5
+ let processor;
6
+
7
+ beforeEach(() => {
8
+ processor = new MessageProcessor();
9
+ });
10
+
11
+ it('should initialize with empty state', () => {
12
+ const result = processor.getMessage();
13
+ expect(result.groupedPackets).toEqual([]);
14
+ expect(result.toolPackets).toEqual([]);
15
+ expect(result.displayPackets).toEqual([]);
16
+ expect(result.citations || {}).toEqual({});
17
+ expect(result.documents).toEqual(null);
18
+ expect(result.isComplete).toBe(false);
19
+ });
20
+
21
+ it('should process text message packets', () => {
22
+ const packets = [
23
+ {
24
+ ind: 0,
25
+ obj: {
26
+ type: PacketType.MESSAGE_START,
27
+ id: 'msg1',
28
+ content: 'Hello world',
29
+ final_documents: null,
30
+ },
31
+ },
32
+ ];
33
+
34
+ processor.addPackets(packets);
35
+ const result = processor.getMessage();
36
+ expect(result.groupedPackets).toHaveLength(1);
37
+ expect(result.displayPackets).toContain(0);
38
+ expect(result.groupedPackets[0].packets[0].obj.content).toBe('Hello world');
39
+ });
40
+
41
+ it('should process search tool packets', () => {
42
+ const packets = [
43
+ {
44
+ ind: 0,
45
+ obj: {
46
+ type: PacketType.SEARCH_TOOL_START,
47
+ search_query: 'test query',
48
+ },
49
+ },
50
+ ];
51
+
52
+ processor.addPackets(packets);
53
+ const result = processor.getMessage();
54
+ expect(result.groupedPackets).toHaveLength(1);
55
+ expect(result.toolPackets).toContain(0);
56
+ });
57
+
58
+ it('should process citation packets', () => {
59
+ const packets = [
60
+ {
61
+ ind: 0,
62
+ obj: {
63
+ type: PacketType.CITATION_DELTA,
64
+ citations: [{ citation_num: 1, document_id: 'doc123' }],
65
+ },
66
+ },
67
+ ];
68
+
69
+ processor.addPackets(packets);
70
+ const result = processor.getMessage();
71
+ expect(result.citations).toEqual({ 1: 'doc123' });
72
+ });
73
+
74
+ it('should process document packets', () => {
75
+ const packets = [
76
+ {
77
+ ind: 0,
78
+ obj: {
79
+ type: PacketType.SEARCH_TOOL_DELTA,
80
+ documents: [
81
+ {
82
+ document_id: 'doc123',
83
+ semantic_identifier: 'Test Document',
84
+ link: 'https://example.com',
85
+ },
86
+ ],
87
+ },
88
+ },
89
+ ];
90
+
91
+ processor.addPackets(packets);
92
+ const result = processor.getMessage();
93
+ expect(result.documents).toHaveLength(1);
94
+ expect(result.documents[0].document_id).toBe('doc123');
95
+ });
96
+
97
+ it('should process error packets', () => {
98
+ const packets = [
99
+ {
100
+ ind: 0,
101
+ obj: {
102
+ type: PacketType.ERROR,
103
+ error: 'Something went wrong',
104
+ },
105
+ },
106
+ ];
107
+
108
+ processor.addPackets(packets);
109
+ const result = processor.getMessage();
110
+ expect(result.error).toBe('Something went wrong');
111
+ });
112
+
113
+ it('should mark as complete when stop packet received', () => {
114
+ const packets = [
115
+ {
116
+ ind: 0,
117
+ obj: {
118
+ type: PacketType.MESSAGE_DELTA,
119
+ content: 'Hello',
120
+ },
121
+ },
122
+ {
123
+ ind: 1,
124
+ obj: {
125
+ type: PacketType.STOP,
126
+ },
127
+ },
128
+ ];
129
+
130
+ processor.addPackets(packets);
131
+ const result = processor.getMessage();
132
+ expect(result.isComplete).toBe(true);
133
+ });
134
+
135
+ it('should reset processor state', () => {
136
+ const packets = [
137
+ {
138
+ ind: 0,
139
+ obj: {
140
+ type: PacketType.MESSAGE_DELTA,
141
+ content: 'Hello',
142
+ },
143
+ },
144
+ ];
145
+
146
+ processor.addPackets(packets);
147
+ processor.reset();
148
+
149
+ const result = processor.getMessage();
150
+ expect(result.groupedPackets).toEqual([]);
151
+ expect(result.documents).toEqual(null);
152
+ expect(result.citations || {}).toEqual({});
153
+ });
154
+ });
@@ -0,0 +1,166 @@
1
+ import '@testing-library/jest-dom/extend-expect';
2
+ import { ChatBlockSchema } from '../schema';
3
+
4
+ describe('ChatBlockSchema', () => {
5
+ const mockAssistants = [
6
+ { id: 1, name: 'Assistant 1' },
7
+ { id: 2, name: 'Assistant 2' },
8
+ ];
9
+
10
+ it('returns schema object with title', () => {
11
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
12
+ expect(schema.title).toBe('Chatbot');
13
+ });
14
+
15
+ it('returns schema with fieldsets', () => {
16
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
17
+ expect(schema.fieldsets).toBeDefined();
18
+ expect(Array.isArray(schema.fieldsets)).toBe(true);
19
+ expect(schema.fieldsets.length).toBeGreaterThan(0);
20
+ });
21
+
22
+ it('returns schema with properties', () => {
23
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
24
+ expect(schema.properties).toBeDefined();
25
+ });
26
+
27
+ it('includes assistant choices from assistants array', () => {
28
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
29
+ expect(schema.properties.assistant.choices).toEqual([
30
+ ['1', 'Assistant 1'],
31
+ ['2', 'Assistant 2'],
32
+ ]);
33
+ });
34
+
35
+ it('handles empty assistants array', () => {
36
+ const schema = ChatBlockSchema({ assistants: [], data: {} });
37
+ expect(schema.properties.assistant.choices).toEqual([]);
38
+ });
39
+
40
+ it('handles undefined assistants', () => {
41
+ const schema = ChatBlockSchema({ assistants: undefined, data: {} });
42
+ expect(schema.properties.assistant.choices).toEqual([]);
43
+ });
44
+
45
+ it('includes starterPrompts field when enableStarterPrompts is true', () => {
46
+ const schema = ChatBlockSchema({
47
+ assistants: mockAssistants,
48
+ data: { enableStarterPrompts: true },
49
+ });
50
+ expect(schema.fieldsets[0].fields).toContain('starterPrompts');
51
+ });
52
+
53
+ it('excludes starterPrompts field when enableStarterPrompts is false', () => {
54
+ const schema = ChatBlockSchema({
55
+ assistants: mockAssistants,
56
+ data: { enableStarterPrompts: false },
57
+ });
58
+ expect(schema.fieldsets[0].fields).not.toContain('starterPrompts');
59
+ });
60
+
61
+ it('includes feedbackReasons field when enableFeedback is true', () => {
62
+ const schema = ChatBlockSchema({
63
+ assistants: mockAssistants,
64
+ data: { enableFeedback: true },
65
+ });
66
+ expect(schema.fieldsets[0].fields).toContain('feedbackReasons');
67
+ });
68
+
69
+ it('excludes feedbackReasons field when enableFeedback is false', () => {
70
+ const schema = ChatBlockSchema({
71
+ assistants: mockAssistants,
72
+ data: { enableFeedback: false },
73
+ });
74
+ expect(schema.fieldsets[0].fields).not.toContain('feedbackReasons');
75
+ });
76
+
77
+ it('includes quality check fields when qualityCheck is enabled', () => {
78
+ const schema = ChatBlockSchema({
79
+ assistants: mockAssistants,
80
+ data: { qualityCheck: 'enabled' },
81
+ });
82
+ expect(schema.fieldsets[0].fields).toContain('maxContextSegments');
83
+ expect(schema.fieldsets[0].fields).toContain('noSupportDocumentsMessage');
84
+ expect(schema.fieldsets[0].fields).toContain('qualityCheckContext');
85
+ expect(schema.fieldsets[0].fields).toContain('qualityCheckStages');
86
+ });
87
+
88
+ it('excludes quality check fields when qualityCheck is disabled', () => {
89
+ const schema = ChatBlockSchema({
90
+ assistants: mockAssistants,
91
+ data: { qualityCheck: 'disabled' },
92
+ });
93
+ expect(schema.fieldsets[0].fields).not.toContain('maxContextSegments');
94
+ expect(schema.fieldsets[0].fields).not.toContain('qualityCheckContext');
95
+ });
96
+
97
+ it('includes onDemandInputToggle when qualityCheck is ondemand_toggle', () => {
98
+ const schema = ChatBlockSchema({
99
+ assistants: mockAssistants,
100
+ data: { qualityCheck: 'ondemand_toggle' },
101
+ });
102
+ expect(schema.fieldsets[0].fields).toContain('onDemandInputToggle');
103
+ });
104
+
105
+ it('excludes onDemandInputToggle when qualityCheck is not ondemand_toggle', () => {
106
+ const schema = ChatBlockSchema({
107
+ assistants: mockAssistants,
108
+ data: { qualityCheck: 'enabled' },
109
+ });
110
+ expect(schema.fieldsets[0].fields).not.toContain('onDemandInputToggle');
111
+ });
112
+
113
+ it('includes totalFailMessage when enableShowTotalFailMessage is true', () => {
114
+ const schema = ChatBlockSchema({
115
+ assistants: mockAssistants,
116
+ data: { enableShowTotalFailMessage: true },
117
+ });
118
+ expect(schema.fieldsets[0].fields).toContain('totalFailMessage');
119
+ });
120
+
121
+ it('excludes totalFailMessage when enableShowTotalFailMessage is false', () => {
122
+ const schema = ChatBlockSchema({
123
+ assistants: mockAssistants,
124
+ data: { enableShowTotalFailMessage: false },
125
+ });
126
+ expect(schema.fieldsets[0].fields).not.toContain('totalFailMessage');
127
+ });
128
+
129
+ it('has required array', () => {
130
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
131
+ expect(schema.required).toBeDefined();
132
+ expect(Array.isArray(schema.required)).toBe(true);
133
+ });
134
+
135
+ it('has default values for key properties', () => {
136
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
137
+ expect(schema.properties.placeholderPrompt.default).toBe('Ask a question');
138
+ expect(schema.properties.chatTitle.default).toBe('Online public chat');
139
+ expect(schema.properties.enableFeedback.default).toBe(true);
140
+ expect(schema.properties.qualityCheck.default).toBe('disabled');
141
+ });
142
+
143
+ it('has qualityCheckStages with default score ranges', () => {
144
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
145
+ expect(schema.properties.qualityCheckStages.default).toBeDefined();
146
+ expect(Array.isArray(schema.properties.qualityCheckStages.default)).toBe(
147
+ true,
148
+ );
149
+ expect(schema.properties.qualityCheckStages.default.length).toBe(5);
150
+ });
151
+
152
+ it('has feedbackReasons with default choices', () => {
153
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
154
+ expect(schema.properties.feedbackReasons.default).toBeDefined();
155
+ expect(Array.isArray(schema.properties.feedbackReasons.default)).toBe(true);
156
+ });
157
+
158
+ it('has deepResearch property with choices', () => {
159
+ const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
160
+ expect(schema.properties.deepResearch.choices).toBeDefined();
161
+ expect(schema.properties.deepResearch.choices).toContainEqual([
162
+ 'always_on',
163
+ 'Always on',
164
+ ]);
165
+ });
166
+ });
@@ -0,0 +1,107 @@
1
+ import { renderHook } from '@testing-library/react-hooks';
2
+ import '@testing-library/jest-dom/extend-expect';
3
+ import { useDeepCompareMemoize } from '../hooks/useDeepCompareMemoize';
4
+
5
+ describe('useDeepCompareMemoize', () => {
6
+ it('returns the same reference for equal objects', () => {
7
+ const deps1 = { a: 1, b: 2 };
8
+ const deps2 = { a: 1, b: 2 };
9
+
10
+ const { result, rerender } = renderHook(
11
+ ({ deps }) => useDeepCompareMemoize(deps),
12
+ { initialProps: { deps: deps1 } },
13
+ );
14
+
15
+ const firstResult = result.current;
16
+
17
+ rerender({ deps: deps2 });
18
+
19
+ expect(result.current).toBe(firstResult);
20
+ });
21
+
22
+ it('returns new reference for different objects', () => {
23
+ const deps1 = { a: 1, b: 2 };
24
+ const deps2 = { a: 1, b: 3 };
25
+
26
+ const { result, rerender } = renderHook(
27
+ ({ deps }) => useDeepCompareMemoize(deps),
28
+ { initialProps: { deps: deps1 } },
29
+ );
30
+
31
+ const firstResult = result.current;
32
+
33
+ rerender({ deps: deps2 });
34
+
35
+ expect(result.current).not.toBe(firstResult);
36
+ expect(result.current).toEqual(deps2);
37
+ });
38
+
39
+ it('handles arrays correctly', () => {
40
+ const deps1 = [1, 2, 3];
41
+ const deps2 = [1, 2, 3];
42
+ const deps3 = [1, 2, 4];
43
+
44
+ const { result, rerender } = renderHook(
45
+ ({ deps }) => useDeepCompareMemoize(deps),
46
+ { initialProps: { deps: deps1 } },
47
+ );
48
+
49
+ const firstResult = result.current;
50
+
51
+ rerender({ deps: deps2 });
52
+ expect(result.current).toBe(firstResult);
53
+
54
+ rerender({ deps: deps3 });
55
+ expect(result.current).not.toBe(firstResult);
56
+ expect(result.current).toEqual(deps3);
57
+ });
58
+
59
+ it('handles nested objects', () => {
60
+ const deps1 = { a: { b: { c: 1 } } };
61
+ const deps2 = { a: { b: { c: 1 } } };
62
+ const deps3 = { a: { b: { c: 2 } } };
63
+
64
+ const { result, rerender } = renderHook(
65
+ ({ deps }) => useDeepCompareMemoize(deps),
66
+ { initialProps: { deps: deps1 } },
67
+ );
68
+
69
+ const firstResult = result.current;
70
+
71
+ rerender({ deps: deps2 });
72
+ expect(result.current).toBe(firstResult);
73
+
74
+ rerender({ deps: deps3 });
75
+ expect(result.current).not.toBe(firstResult);
76
+ });
77
+
78
+ it('handles primitive values', () => {
79
+ const { result, rerender } = renderHook(
80
+ ({ deps }) => useDeepCompareMemoize(deps),
81
+ { initialProps: { deps: 'test' } },
82
+ );
83
+
84
+ const firstResult = result.current;
85
+
86
+ rerender({ deps: 'test' });
87
+ expect(result.current).toBe(firstResult);
88
+
89
+ rerender({ deps: 'different' });
90
+ expect(result.current).not.toBe(firstResult);
91
+ });
92
+
93
+ it('handles null and undefined', () => {
94
+ const { result, rerender } = renderHook(
95
+ ({ deps }) => useDeepCompareMemoize(deps),
96
+ { initialProps: { deps: null } },
97
+ );
98
+
99
+ const firstResult = result.current;
100
+
101
+ rerender({ deps: null });
102
+ expect(result.current).toBe(firstResult);
103
+
104
+ rerender({ deps: undefined });
105
+ expect(result.current).not.toBe(firstResult);
106
+ });
107
+ });
@@ -0,0 +1,151 @@
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
+ });
@@ -0,0 +1,7 @@
1
+ declare module '*.less' {}
2
+
3
+ declare module '*.svg' {
4
+ const attributes: Record<string, string>;
5
+ const content: string;
6
+ export default content;
7
+ }