Skip to content

Commit 36a9b28

Browse files
test: add MessageThread tests for optimistic editing state and resend flow
1 parent f9acd45 commit 36a9b28

2 files changed

Lines changed: 88 additions & 2 deletions

File tree

frontend/src/components/message/EditableUserMessage.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ export const EditableUserMessage = memo(function EditableUserMessage({
8888
onChange={(e) => setEditedContent(e.target.value)}
8989
onKeyDown={handleKeyDown}
9090
onFocus={() => setIsEditingMessage(true)}
91-
onBlur={() => setIsEditingMessage(false)}
9291
className="w-full p-3 rounded-lg bg-background border border-primary/50 focus:border-primary focus:ring-1 focus:ring-primary outline-none resize-none min-h-[60px] text-[16px] md:text-sm"
9392
placeholder="Edit your message..."
9493
disabled={refreshMessage.isPending}
@@ -158,4 +157,4 @@ export const ClickableUserMessage = memo(function ClickableUserMessage({
158157
<Pencil className={`w-4 h-4 flex-shrink-0 mt-0.5 text-muted-foreground transition-all ${isMobile ? 'opacity-100' : 'opacity-50 group-hover/edit:opacity-100 group-hover/edit:text-primary'}`} />
159158
</button>
160159
)
161-
})
160+
})

frontend/src/components/message/MessageThread.test.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest'
22
import { render, screen, fireEvent } from '@testing-library/react'
33
import { MessageThread } from './MessageThread'
4+
import { useUIState } from '@/stores/uiStateStore'
45

56
const mocks = vi.hoisted(() => ({
67
useSessionStatus: vi.fn(),
78
useSessionTodos: vi.fn(),
89
useSettings: vi.fn(),
910
usePermissions: vi.fn(),
1011
useQuestions: vi.fn(),
12+
useRefreshMessage: vi.fn(),
13+
useSessionAgent: vi.fn(),
1114
}))
1215

1316
vi.mock('@/stores/sessionStatusStore', () => ({
@@ -27,6 +30,14 @@ vi.mock('@/contexts/EventContext', () => ({
2730
useQuestions: () => mocks.useQuestions(),
2831
}))
2932

33+
vi.mock('@/hooks/useRemoveMessage', () => ({
34+
useRefreshMessage: () => mocks.useRefreshMessage(),
35+
}))
36+
37+
vi.mock('@/hooks/useSessionAgent', () => ({
38+
useSessionAgent: () => mocks.useSessionAgent(),
39+
}))
40+
3041
interface MockSettingsReturn {
3142
preferences: {
3243
simpleChatMode: boolean
@@ -147,6 +158,12 @@ describe('MessageThread', () => {
147158
mocks.useQuestions.mockReturnValue({
148159
getForCallID: vi.fn(() => null),
149160
})
161+
mocks.useRefreshMessage.mockReturnValue({
162+
isPending: false,
163+
mutate: vi.fn(),
164+
})
165+
mocks.useSessionAgent.mockReturnValue({ agent: 'test-agent' })
166+
useUIState.getState().setIsEditingMessage(false)
150167
})
151168

152169
it('renders assistant message with only subtask part as standalone row without header', () => {
@@ -397,4 +414,74 @@ describe('MessageThread', () => {
397414
expect(screen.getByText('Hello')).toBeInTheDocument()
398415
expect(screen.getByText('You')).toBeInTheDocument()
399416
})
417+
418+
it('keeps global editing state active when edit textarea blurs', () => {
419+
setupSettings({
420+
simpleChatMode: false,
421+
showReasoning: false,
422+
})
423+
424+
const messages = [
425+
createUserMessage('1', 'Hello'),
426+
createAssistantMessage('2', [createTextPart('This is a response', '2')]),
427+
]
428+
429+
const { unmount } = render(
430+
<MessageThread
431+
opcodeUrl="http://localhost:5551"
432+
sessionID="test-session"
433+
messages={messages as any}
434+
/>
435+
)
436+
437+
fireEvent.click(screen.getByTitle('Edit message'))
438+
const textarea = screen.getByPlaceholderText('Edit your message...')
439+
fireEvent.focus(textarea)
440+
expect(useUIState.getState().isEditingMessage).toBe(true)
441+
442+
fireEvent.blur(textarea)
443+
expect(useUIState.getState().isEditingMessage).toBe(true)
444+
445+
unmount()
446+
expect(useUIState.getState().isEditingMessage).toBe(false)
447+
})
448+
449+
it('resends an edited prompt after the edit textarea blurs', () => {
450+
setupSettings({
451+
simpleChatMode: false,
452+
showReasoning: false,
453+
})
454+
const mutate = vi.fn()
455+
mocks.useRefreshMessage.mockReturnValue({
456+
isPending: false,
457+
mutate,
458+
})
459+
460+
const messages = [
461+
createUserMessage('1', 'Hello'),
462+
createAssistantMessage('2', [createTextPart('This is a response', '2')]),
463+
]
464+
465+
render(
466+
<MessageThread
467+
opcodeUrl="http://localhost:5551"
468+
sessionID="test-session"
469+
messages={messages as any}
470+
/>
471+
)
472+
473+
fireEvent.click(screen.getByTitle('Edit message'))
474+
const textarea = screen.getByPlaceholderText('Edit your message...')
475+
fireEvent.change(textarea, { target: { value: 'Updated prompt' } })
476+
fireEvent.blur(textarea)
477+
fireEvent.click(screen.getByRole('button', { name: /resend/i }))
478+
479+
expect(mutate).toHaveBeenCalledWith(
480+
expect.objectContaining({
481+
assistantMessageID: '2',
482+
userMessageContent: 'Updated prompt',
483+
}),
484+
expect.any(Object),
485+
)
486+
})
400487
})

0 commit comments

Comments
 (0)