|
1 | 1 | /** @vitest-environment jsdom */ |
2 | | -import { act, type ComponentProps, useState } from 'react' |
| 2 | +import { act, type ComponentProps, useEffect, useState } from 'react' |
| 3 | +import { ToastProvider } from '@sim/emcn' |
3 | 4 | import { |
4 | 5 | createMockDeploymentShape, |
5 | 6 | deploymentShapeMock, |
@@ -85,6 +86,7 @@ vi.mock('@/hooks/use-chat-input-focus', () => ({ useChatInputFocus: vi.fn() })) |
85 | 86 | vi.mock('@/app/o/[organizationId]/providers/organization-provider', () => organizationProviderMock) |
86 | 87 |
|
87 | 88 | import { Composer } from '@/app/o/[organizationId]/home/components/composer/composer' |
| 89 | +import type { ChatRequestMode } from '@/app/workspace/[workspaceId]/home/types' |
88 | 90 | import { FeatureFlagsProvider } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider' |
89 | 91 | import { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments' |
90 | 92 |
|
@@ -187,6 +189,7 @@ beforeEach(() => { |
187 | 189 |
|
188 | 190 | afterEach(async () => { |
189 | 191 | await act(async () => root.unmount()) |
| 192 | + vi.useRealTimers() |
190 | 193 | container.remove() |
191 | 194 | queryClient.clear() |
192 | 195 | }) |
@@ -251,6 +254,109 @@ function fileList(files: File[]): FileList { |
251 | 254 | return Object.assign(files, { item: (index: number) => files[index] ?? null }) |
252 | 255 | } |
253 | 256 |
|
| 257 | +it.each([ |
| 258 | + { searchEnabled: true, planEnabled: true, modes: ['assistant', 'agent', 'plan', 'assistant'] }, |
| 259 | + { searchEnabled: true, planEnabled: false, modes: ['assistant', 'agent', 'assistant'] }, |
| 260 | + { searchEnabled: false, planEnabled: true, modes: ['agent', 'plan', 'agent'] }, |
| 261 | + { searchEnabled: true, planEnabled: true, withDocument: true, modes: ['agent', 'plan', 'agent'] }, |
| 262 | +] satisfies { |
| 263 | + searchEnabled: boolean |
| 264 | + planEnabled: boolean |
| 265 | + withDocument?: boolean |
| 266 | + modes: ChatRequestMode[] |
| 267 | +}[])( |
| 268 | + 'cycles available modes without losing the draft or selection (Search: $searchEnabled, Plan: $planEnabled, document: $withDocument)', |
| 269 | + async ({ searchEnabled, planEnabled, modes, withDocument = false }) => { |
| 270 | + vi.useFakeTimers({ toFake: ['requestAnimationFrame', 'cancelAnimationFrame'] }) |
| 271 | + let currentMode = modes[0] |
| 272 | + function Harness() { |
| 273 | + const [mode, setMode] = useState(modes[0]) |
| 274 | + currentMode = mode |
| 275 | + const [value, setValue] = useState('Summarize this draft') |
| 276 | + const files = useFileAttachments({ |
| 277 | + userId: 'user-a', |
| 278 | + organizationId: 'organization-a', |
| 279 | + requestMode: mode, |
| 280 | + }) |
| 281 | + const { restoreAttachedFiles } = files |
| 282 | + useEffect(() => { |
| 283 | + if (withDocument) { |
| 284 | + restoreAttachedFiles([ |
| 285 | + { |
| 286 | + id: 'document-a', |
| 287 | + name: 'Draft.pdf', |
| 288 | + type: 'application/pdf', |
| 289 | + size: 1024, |
| 290 | + key: 'sample/draft.pdf', |
| 291 | + path: '', |
| 292 | + uploading: false, |
| 293 | + }, |
| 294 | + ]) |
| 295 | + } |
| 296 | + }, [restoreAttachedFiles]) |
| 297 | + return ( |
| 298 | + <Composer |
| 299 | + requestMode={mode} |
| 300 | + searchEnabled={searchEnabled} |
| 301 | + showModeSelector |
| 302 | + onModeChange={setMode} |
| 303 | + value={value} |
| 304 | + onChange={setValue} |
| 305 | + files={files} |
| 306 | + isInitialView |
| 307 | + isSending={false} |
| 308 | + onStop={() => {}} |
| 309 | + onSubmit={() => {}} |
| 310 | + /> |
| 311 | + ) |
| 312 | + } |
| 313 | + await act(async () => |
| 314 | + root.render( |
| 315 | + <QueryClientProvider client={queryClient}> |
| 316 | + <FeatureFlagsProvider |
| 317 | + flags={{ |
| 318 | + dashboards: false, |
| 319 | + 'table-row-ttl': false, |
| 320 | + 'mothership-model-selector': false, |
| 321 | + 'mothership-plan-mode': planEnabled, |
| 322 | + }} |
| 323 | + > |
| 324 | + <Harness /> |
| 325 | + <ToastProvider /> |
| 326 | + </FeatureFlagsProvider> |
| 327 | + </QueryClientProvider> |
| 328 | + ) |
| 329 | + ) |
| 330 | + const input = container.querySelector<HTMLTextAreaElement>('[aria-label="Ask Sim"]')! |
| 331 | + await act(async () => { |
| 332 | + input.focus() |
| 333 | + input.setSelectionRange(10, 14, 'backward') |
| 334 | + }) |
| 335 | + for (const expectedMode of modes.slice(1)) { |
| 336 | + const event = new KeyboardEvent('keydown', { |
| 337 | + key: 'Tab', |
| 338 | + shiftKey: true, |
| 339 | + bubbles: true, |
| 340 | + cancelable: true, |
| 341 | + }) |
| 342 | + await act(async () => { |
| 343 | + container.querySelector<HTMLTextAreaElement>('[aria-label="Ask Sim"]')!.dispatchEvent(event) |
| 344 | + }) |
| 345 | + await act(async () => vi.advanceTimersToNextFrame()) |
| 346 | + const nextInput = container.querySelector<HTMLTextAreaElement>('[aria-label="Ask Sim"]')! |
| 347 | + expect(currentMode).toBe(expectedMode) |
| 348 | + expect(event.defaultPrevented).toBe(true) |
| 349 | + expect(document.activeElement).toBe(nextInput) |
| 350 | + expect(nextInput.value).toBe('Summarize this draft') |
| 351 | + expect([ |
| 352 | + nextInput.selectionStart, |
| 353 | + nextInput.selectionEnd, |
| 354 | + nextInput.selectionDirection, |
| 355 | + ]).toEqual([10, 14, 'backward']) |
| 356 | + } |
| 357 | + } |
| 358 | +) |
| 359 | + |
254 | 360 | async function paste(files: File[]) { |
255 | 361 | const event = new Event('paste', { bubbles: true, cancelable: true }) |
256 | 362 | Object.defineProperty(event, 'clipboardData', { |
|
0 commit comments