@object-ui/plugin-chatbot 3.1.4 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +40 -9
- package/CHANGELOG.md +27 -0
- package/README.md +86 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11875 -2897
- package/dist/index.umd.cjs +71 -28
- package/dist/{src → packages/plugin-chatbot/src}/ChatbotEnhanced.d.ts +8 -0
- package/dist/packages/plugin-chatbot/src/ChatbotEnhanced.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts +15 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts +29 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts +37 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts +21 -0
- package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts.map +1 -0
- package/dist/{src → packages/plugin-chatbot/src}/index.d.ts +10 -0
- package/dist/packages/plugin-chatbot/src/index.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/renderer.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts +112 -0
- package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts.map +1 -0
- package/dist/packages/plugin-chatbot/src/utils.d.ts.map +1 -0
- package/package.json +10 -8
- package/src/ChatbotEnhanced.tsx +55 -4
- package/src/FloatingChatbot.tsx +89 -0
- package/src/FloatingChatbotPanel.tsx +102 -0
- package/src/FloatingChatbotProvider.tsx +80 -0
- package/src/FloatingChatbotTrigger.tsx +55 -0
- package/src/__tests__/ChatbotEnhancedStreaming.test.tsx +159 -0
- package/src/__tests__/FloatingChatbotProvider.test.tsx +134 -0
- package/src/__tests__/FloatingChatbotWidgets.test.tsx +165 -0
- package/src/__tests__/useObjectChat.test.tsx +347 -0
- package/src/index.tsx +19 -0
- package/src/renderer.tsx +261 -92
- package/src/useObjectChat.ts +344 -0
- package/vite.config.ts +2 -1
- package/dist/src/ChatbotEnhanced.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/renderer.d.ts.map +0 -1
- package/dist/src/utils.d.ts.map +0 -1
- /package/dist/{src → packages/plugin-chatbot/src}/renderer.d.ts +0 -0
- /package/dist/{src → packages/plugin-chatbot/src}/utils.d.ts +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
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 * as React from "react"
|
|
10
|
+
|
|
11
|
+
export interface FloatingChatbotState {
|
|
12
|
+
/** Whether the floating panel is currently open */
|
|
13
|
+
isOpen: boolean
|
|
14
|
+
/** Whether the panel is in fullscreen mode */
|
|
15
|
+
isFullscreen: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FloatingChatbotActions {
|
|
19
|
+
/** Open the floating panel */
|
|
20
|
+
open: () => void
|
|
21
|
+
/** Close the floating panel */
|
|
22
|
+
close: () => void
|
|
23
|
+
/** Toggle the floating panel open/closed */
|
|
24
|
+
toggle: () => void
|
|
25
|
+
/** Toggle fullscreen mode */
|
|
26
|
+
toggleFullscreen: () => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FloatingChatbotContextValue = FloatingChatbotState & FloatingChatbotActions
|
|
30
|
+
|
|
31
|
+
const FloatingChatbotContext = React.createContext<FloatingChatbotContextValue | null>(null)
|
|
32
|
+
|
|
33
|
+
export interface FloatingChatbotProviderProps {
|
|
34
|
+
/** Whether the panel is open by default */
|
|
35
|
+
defaultOpen?: boolean
|
|
36
|
+
children: React.ReactNode
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function FloatingChatbotProvider({
|
|
40
|
+
defaultOpen = false,
|
|
41
|
+
children,
|
|
42
|
+
}: FloatingChatbotProviderProps) {
|
|
43
|
+
const [isOpen, setIsOpen] = React.useState(defaultOpen)
|
|
44
|
+
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
|
45
|
+
|
|
46
|
+
const value = React.useMemo<FloatingChatbotContextValue>(
|
|
47
|
+
() => ({
|
|
48
|
+
isOpen,
|
|
49
|
+
isFullscreen,
|
|
50
|
+
open: () => setIsOpen(true),
|
|
51
|
+
close: () => {
|
|
52
|
+
setIsOpen(false)
|
|
53
|
+
setIsFullscreen(false)
|
|
54
|
+
},
|
|
55
|
+
toggle: () => setIsOpen((prev) => !prev),
|
|
56
|
+
toggleFullscreen: () => setIsFullscreen((prev) => !prev),
|
|
57
|
+
}),
|
|
58
|
+
[isOpen, isFullscreen]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<FloatingChatbotContext.Provider value={value}>
|
|
63
|
+
{children}
|
|
64
|
+
</FloatingChatbotContext.Provider>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook to access the floating chatbot state and actions.
|
|
70
|
+
* Must be used within a `FloatingChatbotProvider`.
|
|
71
|
+
*/
|
|
72
|
+
export function useFloatingChatbot(): FloatingChatbotContextValue {
|
|
73
|
+
const context = React.useContext(FloatingChatbotContext)
|
|
74
|
+
if (!context) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"useFloatingChatbot must be used within a <FloatingChatbotProvider>"
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return context
|
|
80
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 * as React from "react"
|
|
10
|
+
import { cn } from "@object-ui/components"
|
|
11
|
+
import { Button } from "@object-ui/components"
|
|
12
|
+
import { MessageCircle, X } from "lucide-react"
|
|
13
|
+
import { useFloatingChatbot } from "./FloatingChatbotProvider"
|
|
14
|
+
|
|
15
|
+
export interface FloatingChatbotTriggerProps {
|
|
16
|
+
/** Position of the FAB */
|
|
17
|
+
position?: "bottom-right" | "bottom-left"
|
|
18
|
+
/** Size of the trigger button in pixels */
|
|
19
|
+
size?: number
|
|
20
|
+
/** Custom className */
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Floating Action Button (FAB) trigger for the chatbot.
|
|
26
|
+
* Renders a circular button fixed to the viewport corner.
|
|
27
|
+
*/
|
|
28
|
+
export function FloatingChatbotTrigger({
|
|
29
|
+
position = "bottom-right",
|
|
30
|
+
size = 56,
|
|
31
|
+
className,
|
|
32
|
+
}: FloatingChatbotTriggerProps) {
|
|
33
|
+
const { isOpen, toggle } = useFloatingChatbot()
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Button
|
|
37
|
+
onClick={toggle}
|
|
38
|
+
className={cn(
|
|
39
|
+
"fixed z-50 rounded-full shadow-lg transition-transform hover:scale-105",
|
|
40
|
+
position === "bottom-right" ? "right-6 bottom-6" : "left-6 bottom-6",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
style={{ width: size, height: size }}
|
|
44
|
+
size="icon"
|
|
45
|
+
aria-label={isOpen ? "Close chat" : "Open chat"}
|
|
46
|
+
data-testid="floating-chatbot-trigger"
|
|
47
|
+
>
|
|
48
|
+
{isOpen ? (
|
|
49
|
+
<X className="h-6 w-6" />
|
|
50
|
+
) : (
|
|
51
|
+
<MessageCircle className="h-6 w-6" />
|
|
52
|
+
)}
|
|
53
|
+
</Button>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
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 } from '@testing-library/react';
|
|
12
|
+
import { ChatbotEnhanced, type ChatMessage } from '../ChatbotEnhanced';
|
|
13
|
+
|
|
14
|
+
describe('ChatbotEnhanced - Streaming & AI Features', () => {
|
|
15
|
+
const mockMessages: ChatMessage[] = [
|
|
16
|
+
{ id: '1', role: 'user', content: 'Hello!' },
|
|
17
|
+
{ id: '2', role: 'assistant', content: 'Hi there!' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
it('should show stop button when isLoading and onStop is provided', () => {
|
|
21
|
+
const onStop = vi.fn();
|
|
22
|
+
render(
|
|
23
|
+
<ChatbotEnhanced
|
|
24
|
+
messages={mockMessages}
|
|
25
|
+
isLoading={true}
|
|
26
|
+
onStop={onStop}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const stopButton = screen.getByRole('button', { name: /stop/i });
|
|
31
|
+
expect(stopButton).toBeInTheDocument();
|
|
32
|
+
|
|
33
|
+
fireEvent.click(stopButton);
|
|
34
|
+
expect(onStop).toHaveBeenCalledTimes(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should not show stop button when not loading', () => {
|
|
38
|
+
const onStop = vi.fn();
|
|
39
|
+
render(
|
|
40
|
+
<ChatbotEnhanced
|
|
41
|
+
messages={mockMessages}
|
|
42
|
+
isLoading={false}
|
|
43
|
+
onStop={onStop}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(screen.queryByRole('button', { name: /stop/i })).not.toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should not show stop button when onStop is not provided', () => {
|
|
51
|
+
render(
|
|
52
|
+
<ChatbotEnhanced
|
|
53
|
+
messages={mockMessages}
|
|
54
|
+
isLoading={true}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(screen.queryByRole('button', { name: /stop/i })).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should show error message when error is provided', () => {
|
|
62
|
+
const error = new Error('Connection failed');
|
|
63
|
+
render(
|
|
64
|
+
<ChatbotEnhanced
|
|
65
|
+
messages={mockMessages}
|
|
66
|
+
error={error}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByText('Connection failed')).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should show retry button in error state when onReload is provided', () => {
|
|
75
|
+
const onReload = vi.fn();
|
|
76
|
+
const error = new Error('Network error');
|
|
77
|
+
render(
|
|
78
|
+
<ChatbotEnhanced
|
|
79
|
+
messages={mockMessages}
|
|
80
|
+
error={error}
|
|
81
|
+
onReload={onReload}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const retryButton = screen.getByRole('button', { name: /retry/i });
|
|
86
|
+
expect(retryButton).toBeInTheDocument();
|
|
87
|
+
|
|
88
|
+
fireEvent.click(retryButton);
|
|
89
|
+
expect(onReload).toHaveBeenCalledTimes(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should not show retry button when onReload is not provided', () => {
|
|
93
|
+
const error = new Error('Error');
|
|
94
|
+
render(
|
|
95
|
+
<ChatbotEnhanced
|
|
96
|
+
messages={mockMessages}
|
|
97
|
+
error={error}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should disable input when isLoading is true', () => {
|
|
105
|
+
render(
|
|
106
|
+
<ChatbotEnhanced
|
|
107
|
+
messages={mockMessages}
|
|
108
|
+
isLoading={true}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const input = screen.getByPlaceholderText(/message/i);
|
|
113
|
+
expect(input).toBeDisabled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should disable send button when isLoading is true', () => {
|
|
117
|
+
render(
|
|
118
|
+
<ChatbotEnhanced
|
|
119
|
+
messages={mockMessages}
|
|
120
|
+
isLoading={true}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const sendButton = screen.getByRole('button', { name: /send/i });
|
|
125
|
+
expect(sendButton).toBeDisabled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should show streaming indicator on streaming messages', () => {
|
|
129
|
+
const streamingMessages: ChatMessage[] = [
|
|
130
|
+
{
|
|
131
|
+
id: '1',
|
|
132
|
+
role: 'assistant',
|
|
133
|
+
content: 'Generating...',
|
|
134
|
+
streaming: true,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<ChatbotEnhanced messages={streamingMessages} />
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(screen.getByText('Generating...')).toBeInTheDocument();
|
|
143
|
+
// Streaming indicator should be present (bouncing dots)
|
|
144
|
+
const bouncingDots = container.querySelectorAll('.animate-bounce');
|
|
145
|
+
expect(bouncingDots.length).toBe(3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should show default error message when error has no message', () => {
|
|
149
|
+
const error = new Error('');
|
|
150
|
+
render(
|
|
151
|
+
<ChatbotEnhanced
|
|
152
|
+
messages={mockMessages}
|
|
153
|
+
error={error}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(screen.getByText('An error occurred')).toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
});
|