@object-ui/plugin-chatbot 3.1.5 → 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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +39 -8
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +86 -4
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +11875 -2897
  6. package/dist/index.umd.cjs +71 -28
  7. package/dist/{src → packages/plugin-chatbot/src}/ChatbotEnhanced.d.ts +8 -0
  8. package/dist/packages/plugin-chatbot/src/ChatbotEnhanced.d.ts.map +1 -0
  9. package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts +15 -0
  10. package/dist/packages/plugin-chatbot/src/FloatingChatbot.d.ts.map +1 -0
  11. package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts +29 -0
  12. package/dist/packages/plugin-chatbot/src/FloatingChatbotPanel.d.ts.map +1 -0
  13. package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts +37 -0
  14. package/dist/packages/plugin-chatbot/src/FloatingChatbotProvider.d.ts.map +1 -0
  15. package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts +21 -0
  16. package/dist/packages/plugin-chatbot/src/FloatingChatbotTrigger.d.ts.map +1 -0
  17. package/dist/{src → packages/plugin-chatbot/src}/index.d.ts +10 -0
  18. package/dist/packages/plugin-chatbot/src/index.d.ts.map +1 -0
  19. package/dist/packages/plugin-chatbot/src/renderer.d.ts.map +1 -0
  20. package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts +112 -0
  21. package/dist/packages/plugin-chatbot/src/useObjectChat.d.ts.map +1 -0
  22. package/dist/packages/plugin-chatbot/src/utils.d.ts.map +1 -0
  23. package/package.json +10 -8
  24. package/src/ChatbotEnhanced.tsx +55 -4
  25. package/src/FloatingChatbot.tsx +89 -0
  26. package/src/FloatingChatbotPanel.tsx +102 -0
  27. package/src/FloatingChatbotProvider.tsx +80 -0
  28. package/src/FloatingChatbotTrigger.tsx +55 -0
  29. package/src/__tests__/ChatbotEnhancedStreaming.test.tsx +159 -0
  30. package/src/__tests__/FloatingChatbotProvider.test.tsx +134 -0
  31. package/src/__tests__/FloatingChatbotWidgets.test.tsx +165 -0
  32. package/src/__tests__/useObjectChat.test.tsx +347 -0
  33. package/src/index.tsx +19 -0
  34. package/src/renderer.tsx +261 -92
  35. package/src/useObjectChat.ts +344 -0
  36. package/vite.config.ts +2 -1
  37. package/dist/src/ChatbotEnhanced.d.ts.map +0 -1
  38. package/dist/src/index.d.ts.map +0 -1
  39. package/dist/src/renderer.d.ts.map +0 -1
  40. package/dist/src/utils.d.ts.map +0 -1
  41. /package/dist/{src → packages/plugin-chatbot/src}/renderer.d.ts +0 -0
  42. /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
+ });