@object-ui/plugin-chatbot 3.3.0 → 3.3.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.
@@ -1,134 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import '@testing-library/jest-dom';
10
- import { describe, it, expect, vi } from 'vitest';
11
- import { render, screen, fireEvent, act } from '@testing-library/react';
12
- import {
13
- FloatingChatbotProvider,
14
- useFloatingChatbot,
15
- } from '../FloatingChatbotProvider';
16
- import * as React from 'react';
17
-
18
- // Helper to render the hook inside a provider
19
- function TestConsumer() {
20
- const ctx = useFloatingChatbot();
21
- return (
22
- <div>
23
- <span data-testid="isOpen">{String(ctx.isOpen)}</span>
24
- <span data-testid="isFullscreen">{String(ctx.isFullscreen)}</span>
25
- <button data-testid="open" onClick={ctx.open}>Open</button>
26
- <button data-testid="close" onClick={ctx.close}>Close</button>
27
- <button data-testid="toggle" onClick={ctx.toggle}>Toggle</button>
28
- <button data-testid="toggleFs" onClick={ctx.toggleFullscreen}>ToggleFS</button>
29
- </div>
30
- );
31
- }
32
-
33
- describe('FloatingChatbotProvider', () => {
34
- it('defaults to closed', () => {
35
- render(
36
- <FloatingChatbotProvider>
37
- <TestConsumer />
38
- </FloatingChatbotProvider>
39
- );
40
- expect(screen.getByTestId('isOpen').textContent).toBe('false');
41
- expect(screen.getByTestId('isFullscreen').textContent).toBe('false');
42
- });
43
-
44
- it('respects defaultOpen=true', () => {
45
- render(
46
- <FloatingChatbotProvider defaultOpen={true}>
47
- <TestConsumer />
48
- </FloatingChatbotProvider>
49
- );
50
- expect(screen.getByTestId('isOpen').textContent).toBe('true');
51
- });
52
-
53
- it('open() sets isOpen to true', () => {
54
- render(
55
- <FloatingChatbotProvider>
56
- <TestConsumer />
57
- </FloatingChatbotProvider>
58
- );
59
- expect(screen.getByTestId('isOpen').textContent).toBe('false');
60
-
61
- act(() => {
62
- fireEvent.click(screen.getByTestId('open'));
63
- });
64
- expect(screen.getByTestId('isOpen').textContent).toBe('true');
65
- });
66
-
67
- it('close() sets isOpen to false and resets fullscreen', () => {
68
- render(
69
- <FloatingChatbotProvider defaultOpen={true}>
70
- <TestConsumer />
71
- </FloatingChatbotProvider>
72
- );
73
-
74
- // Go fullscreen first
75
- act(() => {
76
- fireEvent.click(screen.getByTestId('toggleFs'));
77
- });
78
- expect(screen.getByTestId('isFullscreen').textContent).toBe('true');
79
-
80
- // Close should reset both
81
- act(() => {
82
- fireEvent.click(screen.getByTestId('close'));
83
- });
84
- expect(screen.getByTestId('isOpen').textContent).toBe('false');
85
- expect(screen.getByTestId('isFullscreen').textContent).toBe('false');
86
- });
87
-
88
- it('toggle() flips isOpen', () => {
89
- render(
90
- <FloatingChatbotProvider>
91
- <TestConsumer />
92
- </FloatingChatbotProvider>
93
- );
94
-
95
- act(() => {
96
- fireEvent.click(screen.getByTestId('toggle'));
97
- });
98
- expect(screen.getByTestId('isOpen').textContent).toBe('true');
99
-
100
- act(() => {
101
- fireEvent.click(screen.getByTestId('toggle'));
102
- });
103
- expect(screen.getByTestId('isOpen').textContent).toBe('false');
104
- });
105
-
106
- it('toggleFullscreen() flips isFullscreen', () => {
107
- render(
108
- <FloatingChatbotProvider defaultOpen={true}>
109
- <TestConsumer />
110
- </FloatingChatbotProvider>
111
- );
112
-
113
- act(() => {
114
- fireEvent.click(screen.getByTestId('toggleFs'));
115
- });
116
- expect(screen.getByTestId('isFullscreen').textContent).toBe('true');
117
-
118
- act(() => {
119
- fireEvent.click(screen.getByTestId('toggleFs'));
120
- });
121
- expect(screen.getByTestId('isFullscreen').textContent).toBe('false');
122
- });
123
- });
124
-
125
- describe('useFloatingChatbot', () => {
126
- it('throws when used outside a provider', () => {
127
- // Suppress console.error for this test
128
- const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
129
- expect(() => render(<TestConsumer />)).toThrow(
130
- 'useFloatingChatbot must be used within a <FloatingChatbotProvider>'
131
- );
132
- spy.mockRestore();
133
- });
134
- });
@@ -1,165 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import '@testing-library/jest-dom';
10
- import { describe, it, expect, vi } from 'vitest';
11
- import { render, screen, fireEvent, act } from '@testing-library/react';
12
- import { FloatingChatbotProvider } from '../FloatingChatbotProvider';
13
- import { FloatingChatbotTrigger } from '../FloatingChatbotTrigger';
14
- import { FloatingChatbotPanel } from '../FloatingChatbotPanel';
15
-
16
- // Wrap in provider helper
17
- function renderWithProvider(
18
- ui: React.ReactElement,
19
- { defaultOpen = false }: { defaultOpen?: boolean } = {}
20
- ) {
21
- return render(
22
- <FloatingChatbotProvider defaultOpen={defaultOpen}>{ui}</FloatingChatbotProvider>
23
- );
24
- }
25
-
26
- describe('FloatingChatbotTrigger', () => {
27
- it('renders a trigger button', () => {
28
- renderWithProvider(<FloatingChatbotTrigger />);
29
- expect(screen.getByTestId('floating-chatbot-trigger')).toBeInTheDocument();
30
- });
31
-
32
- it('shows "Open chat" label when closed', () => {
33
- renderWithProvider(<FloatingChatbotTrigger />);
34
- expect(screen.getByLabelText('Open chat')).toBeInTheDocument();
35
- });
36
-
37
- it('shows "Close chat" label when open', () => {
38
- renderWithProvider(<FloatingChatbotTrigger />, { defaultOpen: true });
39
- expect(screen.getByLabelText('Close chat')).toBeInTheDocument();
40
- });
41
-
42
- it('toggles open state on click', () => {
43
- renderWithProvider(
44
- <>
45
- <FloatingChatbotTrigger />
46
- <FloatingChatbotPanel><span>Panel content</span></FloatingChatbotPanel>
47
- </>
48
- );
49
-
50
- // Panel should not be visible initially
51
- expect(screen.queryByTestId('floating-chatbot-panel')).not.toBeInTheDocument();
52
-
53
- // Click trigger to open
54
- act(() => {
55
- fireEvent.click(screen.getByTestId('floating-chatbot-trigger'));
56
- });
57
- expect(screen.getByTestId('floating-chatbot-panel')).toBeInTheDocument();
58
-
59
- // Click trigger to close
60
- act(() => {
61
- fireEvent.click(screen.getByTestId('floating-chatbot-trigger'));
62
- });
63
- expect(screen.queryByTestId('floating-chatbot-panel')).not.toBeInTheDocument();
64
- });
65
-
66
- it('applies bottom-left position class', () => {
67
- renderWithProvider(<FloatingChatbotTrigger position="bottom-left" />);
68
- const trigger = screen.getByTestId('floating-chatbot-trigger');
69
- expect(trigger.className).toContain('left-6');
70
- });
71
-
72
- it('applies custom size', () => {
73
- renderWithProvider(<FloatingChatbotTrigger size={48} />);
74
- const trigger = screen.getByTestId('floating-chatbot-trigger');
75
- expect(trigger.style.width).toBe('48px');
76
- expect(trigger.style.height).toBe('48px');
77
- });
78
- });
79
-
80
- describe('FloatingChatbotPanel', () => {
81
- it('does not render when closed', () => {
82
- renderWithProvider(
83
- <FloatingChatbotPanel><span>Content</span></FloatingChatbotPanel>
84
- );
85
- expect(screen.queryByTestId('floating-chatbot-panel')).not.toBeInTheDocument();
86
- });
87
-
88
- it('renders when open', () => {
89
- renderWithProvider(
90
- <FloatingChatbotPanel><span>Panel body</span></FloatingChatbotPanel>,
91
- { defaultOpen: true }
92
- );
93
- expect(screen.getByTestId('floating-chatbot-panel')).toBeInTheDocument();
94
- expect(screen.getByText('Panel body')).toBeInTheDocument();
95
- });
96
-
97
- it('renders custom title', () => {
98
- renderWithProvider(
99
- <FloatingChatbotPanel title="Help"><span>Body</span></FloatingChatbotPanel>,
100
- { defaultOpen: true }
101
- );
102
- expect(screen.getByText('Help')).toBeInTheDocument();
103
- });
104
-
105
- it('has dialog role with aria-label', () => {
106
- renderWithProvider(
107
- <FloatingChatbotPanel title="Help"><span>Body</span></FloatingChatbotPanel>,
108
- { defaultOpen: true }
109
- );
110
- const dialog = screen.getByRole('dialog');
111
- expect(dialog).toHaveAttribute('aria-label', 'Help');
112
- });
113
-
114
- it('close button closes the panel', () => {
115
- renderWithProvider(
116
- <FloatingChatbotPanel><span>Body</span></FloatingChatbotPanel>,
117
- { defaultOpen: true }
118
- );
119
- expect(screen.getByTestId('floating-chatbot-panel')).toBeInTheDocument();
120
-
121
- act(() => {
122
- fireEvent.click(screen.getByTestId('floating-chatbot-close'));
123
- });
124
- expect(screen.queryByTestId('floating-chatbot-panel')).not.toBeInTheDocument();
125
- });
126
-
127
- it('fullscreen button toggles fullscreen', () => {
128
- renderWithProvider(
129
- <FloatingChatbotPanel><span>Body</span></FloatingChatbotPanel>,
130
- { defaultOpen: true }
131
- );
132
-
133
- const fsButton = screen.getByTestId('floating-chatbot-fullscreen');
134
- expect(fsButton).toHaveAttribute('aria-label', 'Fullscreen');
135
-
136
- act(() => {
137
- fireEvent.click(fsButton);
138
- });
139
- expect(screen.getByTestId('floating-chatbot-fullscreen')).toHaveAttribute(
140
- 'aria-label',
141
- 'Exit fullscreen'
142
- );
143
- });
144
-
145
- it('applies custom width and height', () => {
146
- renderWithProvider(
147
- <FloatingChatbotPanel width={500} height={600}>
148
- <span>Body</span>
149
- </FloatingChatbotPanel>,
150
- { defaultOpen: true }
151
- );
152
- const panel = screen.getByTestId('floating-chatbot-panel');
153
- expect(panel.style.width).toBe('500px');
154
- expect(panel.style.height).toBe('600px');
155
- });
156
-
157
- it('bottom-left position applies left-6 class', () => {
158
- renderWithProvider(
159
- <FloatingChatbotPanel position="bottom-left"><span>Body</span></FloatingChatbotPanel>,
160
- { defaultOpen: true }
161
- );
162
- const panel = screen.getByTestId('floating-chatbot-panel');
163
- expect(panel.className).toContain('left-6');
164
- });
165
- });
@@ -1,347 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import '@testing-library/jest-dom';
10
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
- import { renderHook, act } from '@testing-library/react';
12
-
13
- // Mock @ai-sdk/react so the static import in useObjectChat doesn't break tests
14
- vi.mock('@ai-sdk/react', () => ({
15
- useChat: vi.fn(() => ({
16
- messages: [],
17
- isLoading: false,
18
- error: undefined,
19
- input: '',
20
- setInput: vi.fn(),
21
- handleInputChange: vi.fn(),
22
- append: vi.fn(),
23
- stop: vi.fn(),
24
- reload: vi.fn(),
25
- setMessages: vi.fn(),
26
- })),
27
- }));
28
-
29
- import { useObjectChat } from '../useObjectChat';
30
-
31
- describe('useObjectChat', () => {
32
- beforeEach(() => {
33
- vi.useFakeTimers();
34
- });
35
-
36
- afterEach(() => {
37
- vi.useRealTimers();
38
- });
39
-
40
- describe('local/legacy mode (no api)', () => {
41
- it('should initialize with empty messages when no initialMessages provided', () => {
42
- const { result } = renderHook(() => useObjectChat());
43
-
44
- expect(result.current.messages).toEqual([]);
45
- expect(result.current.isLoading).toBe(false);
46
- expect(result.current.error).toBeUndefined();
47
- expect(result.current.isApiMode).toBe(false);
48
- expect(result.current.input).toBe('');
49
- });
50
-
51
- it('should initialize with provided messages', () => {
52
- const initialMessages = [
53
- { id: '1', role: 'assistant' as const, content: 'Hello!' },
54
- { id: '2', role: 'user' as const, content: 'Hi!' },
55
- ];
56
-
57
- const { result } = renderHook(() =>
58
- useObjectChat({ initialMessages })
59
- );
60
-
61
- expect(result.current.messages).toHaveLength(2);
62
- expect(result.current.messages[0].content).toBe('Hello!');
63
- expect(result.current.messages[1].content).toBe('Hi!');
64
- });
65
-
66
- it('should add a user message when sendMessage is called', () => {
67
- const { result } = renderHook(() => useObjectChat());
68
-
69
- act(() => {
70
- result.current.sendMessage('Hello world');
71
- });
72
-
73
- expect(result.current.messages).toHaveLength(1);
74
- expect(result.current.messages[0].role).toBe('user');
75
- expect(result.current.messages[0].content).toBe('Hello world');
76
- expect(result.current.messages[0].id).toBeTruthy();
77
- });
78
-
79
- it('should not send empty messages', () => {
80
- const { result } = renderHook(() => useObjectChat());
81
-
82
- act(() => {
83
- result.current.sendMessage('');
84
- });
85
-
86
- expect(result.current.messages).toHaveLength(0);
87
-
88
- act(() => {
89
- result.current.sendMessage(' ');
90
- });
91
-
92
- expect(result.current.messages).toHaveLength(0);
93
- });
94
-
95
- it('should trim whitespace from messages', () => {
96
- const { result } = renderHook(() => useObjectChat());
97
-
98
- act(() => {
99
- result.current.sendMessage(' Hello ');
100
- });
101
-
102
- expect(result.current.messages[0].content).toBe('Hello');
103
- });
104
-
105
- it('should call onSend callback when message is sent', () => {
106
- const onSend = vi.fn();
107
- const { result } = renderHook(() => useObjectChat({ onSend }));
108
-
109
- act(() => {
110
- result.current.sendMessage('Test message');
111
- });
112
-
113
- expect(onSend).toHaveBeenCalledWith(
114
- 'Test message',
115
- expect.arrayContaining([
116
- expect.objectContaining({ content: 'Test message', role: 'user' }),
117
- ])
118
- );
119
- });
120
-
121
- it('should add auto-response after delay when autoResponse is enabled', () => {
122
- const { result } = renderHook(() =>
123
- useObjectChat({
124
- autoResponse: true,
125
- autoResponseText: 'Auto reply!',
126
- autoResponseDelay: 500,
127
- })
128
- );
129
-
130
- act(() => {
131
- result.current.sendMessage('Hello');
132
- });
133
-
134
- expect(result.current.messages).toHaveLength(1);
135
- expect(result.current.isLoading).toBe(true);
136
-
137
- act(() => {
138
- vi.advanceTimersByTime(500);
139
- });
140
-
141
- expect(result.current.messages).toHaveLength(2);
142
- expect(result.current.messages[1].role).toBe('assistant');
143
- expect(result.current.messages[1].content).toBe('Auto reply!');
144
- expect(result.current.isLoading).toBe(false);
145
- });
146
-
147
- it('should use default auto-response text when none provided', () => {
148
- const { result } = renderHook(() =>
149
- useObjectChat({
150
- autoResponse: true,
151
- autoResponseDelay: 500,
152
- })
153
- );
154
-
155
- act(() => {
156
- result.current.sendMessage('Hello');
157
- });
158
-
159
- act(() => {
160
- vi.advanceTimersByTime(500);
161
- });
162
-
163
- expect(result.current.messages[1].content).toBe('Thank you for your message!');
164
- });
165
-
166
- it('should add timestamps when showTimestamp is enabled', () => {
167
- const { result } = renderHook(() =>
168
- useObjectChat({ showTimestamp: true })
169
- );
170
-
171
- act(() => {
172
- result.current.sendMessage('Hello');
173
- });
174
-
175
- expect(result.current.messages[0].timestamp).toBeTruthy();
176
- });
177
-
178
- it('should not add timestamps when showTimestamp is disabled', () => {
179
- const { result } = renderHook(() =>
180
- useObjectChat({ showTimestamp: false })
181
- );
182
-
183
- act(() => {
184
- result.current.sendMessage('Hello');
185
- });
186
-
187
- expect(result.current.messages[0].timestamp).toBeUndefined();
188
- });
189
-
190
- it('should clear all messages when clear is called', () => {
191
- const { result } = renderHook(() =>
192
- useObjectChat({
193
- initialMessages: [
194
- { id: '1', role: 'assistant', content: 'Hello!' },
195
- ],
196
- })
197
- );
198
-
199
- expect(result.current.messages).toHaveLength(1);
200
-
201
- act(() => {
202
- result.current.clear();
203
- });
204
-
205
- expect(result.current.messages).toHaveLength(0);
206
- expect(result.current.isLoading).toBe(false);
207
- });
208
-
209
- it('should stop auto-response timer when stop is called', () => {
210
- const { result } = renderHook(() =>
211
- useObjectChat({
212
- autoResponse: true,
213
- autoResponseDelay: 1000,
214
- })
215
- );
216
-
217
- act(() => {
218
- result.current.sendMessage('Hello');
219
- });
220
-
221
- expect(result.current.isLoading).toBe(true);
222
-
223
- act(() => {
224
- result.current.stop();
225
- });
226
-
227
- expect(result.current.isLoading).toBe(false);
228
-
229
- // Advance time - no auto-response should appear
230
- act(() => {
231
- vi.advanceTimersByTime(2000);
232
- });
233
-
234
- expect(result.current.messages).toHaveLength(1); // Only user message
235
- });
236
-
237
- it('reload should be a no-op in local mode', () => {
238
- const { result } = renderHook(() =>
239
- useObjectChat({
240
- initialMessages: [
241
- { id: '1', role: 'user', content: 'Hello' },
242
- ],
243
- })
244
- );
245
-
246
- act(() => {
247
- result.current.reload();
248
- });
249
-
250
- expect(result.current.messages).toHaveLength(1);
251
- });
252
-
253
- it('should handle input state management', () => {
254
- const { result } = renderHook(() => useObjectChat());
255
-
256
- expect(result.current.input).toBe('');
257
-
258
- act(() => {
259
- result.current.setInput('Hello');
260
- });
261
-
262
- expect(result.current.input).toBe('Hello');
263
- });
264
-
265
- it('should handle input change events', () => {
266
- const { result } = renderHook(() => useObjectChat());
267
-
268
- act(() => {
269
- result.current.handleInputChange({
270
- target: { value: 'Hello world' },
271
- } as React.ChangeEvent<HTMLInputElement>);
272
- });
273
-
274
- expect(result.current.input).toBe('Hello world');
275
- });
276
-
277
- it('should clear input after sending message', () => {
278
- const { result } = renderHook(() => useObjectChat());
279
-
280
- act(() => {
281
- result.current.setInput('Hello');
282
- });
283
-
284
- expect(result.current.input).toBe('Hello');
285
-
286
- act(() => {
287
- result.current.sendMessage('Hello');
288
- });
289
-
290
- expect(result.current.input).toBe('');
291
- });
292
-
293
- it('should normalize initial messages with missing fields', () => {
294
- const { result } = renderHook(() =>
295
- useObjectChat({
296
- initialMessages: [
297
- { id: '', role: 'user' as const, content: '' },
298
- ],
299
- })
300
- );
301
-
302
- expect(result.current.messages[0].id).toBe('msg-0');
303
- expect(result.current.messages[0].content).toBe('');
304
- });
305
-
306
- it('should report isApiMode as false in local mode', () => {
307
- const { result } = renderHook(() => useObjectChat());
308
- expect(result.current.isApiMode).toBe(false);
309
- });
310
-
311
- it('should report isApiMode as false when api is empty string', () => {
312
- const { result } = renderHook(() => useObjectChat({ api: '' }));
313
- expect(result.current.isApiMode).toBe(false);
314
- });
315
-
316
- it('should cancel pending auto-response timer when clear is called', () => {
317
- const { result } = renderHook(() =>
318
- useObjectChat({
319
- autoResponse: true,
320
- autoResponseText: 'Auto reply!',
321
- autoResponseDelay: 1000,
322
- })
323
- );
324
-
325
- act(() => {
326
- result.current.sendMessage('Hello');
327
- });
328
-
329
- expect(result.current.messages).toHaveLength(1);
330
- expect(result.current.isLoading).toBe(true);
331
-
332
- act(() => {
333
- result.current.clear();
334
- });
335
-
336
- expect(result.current.messages).toHaveLength(0);
337
- expect(result.current.isLoading).toBe(false);
338
-
339
- // Advance time - no auto-response should appear after clear
340
- act(() => {
341
- vi.advanceTimersByTime(2000);
342
- });
343
-
344
- expect(result.current.messages).toHaveLength(0);
345
- });
346
- });
347
- });