|
1 | 1 | import { describe, test, expect, vi } from 'vitest'; |
2 | | -import { screen, waitFor } from '@testing-library/svelte'; |
| 2 | +import { screen, waitFor, within } from '@testing-library/svelte'; |
3 | 3 | import { userEvent } from '@testing-library/user-event'; |
4 | 4 | import { renderWithProviders } from './__tests__/render'; |
5 | 5 | import { anAIMessage } from './__tests__/fixtures'; |
@@ -106,8 +106,12 @@ describe('Chat', () => { |
106 | 106 | mockClient, |
107 | 107 | 'test-123', |
108 | 108 | 'assistant-1', |
109 | | - 'Tell me about AI', |
110 | | - expect.any(String) |
| 109 | + expect.objectContaining({ |
| 110 | + type: 'human', |
| 111 | + content: 'Tell me about AI', |
| 112 | + id: expect.any(String) |
| 113 | + }), |
| 114 | + expect.any(AbortSignal) |
111 | 115 | ); |
112 | 116 | }); |
113 | 117 | }); |
@@ -157,6 +161,88 @@ describe('Chat', () => { |
157 | 161 | }); |
158 | 162 | }); |
159 | 163 |
|
| 164 | + describe('when stop is clicked', () => { |
| 165 | + test('stop button aborts the stream signal', async () => { |
| 166 | + const user = userEvent.setup(); |
| 167 | + let capturedSignal: AbortSignal | undefined; |
| 168 | + |
| 169 | + vi.mocked(streamAnswer).mockImplementation(async function* (_c, _t, _a, _im, signal) { |
| 170 | + capturedSignal = signal; |
| 171 | + yield anAIMessage({ text: 'Partial...' }); |
| 172 | + await new Promise<void>((r) => signal.addEventListener('abort', () => r())); |
| 173 | + }); |
| 174 | + |
| 175 | + renderChat(); |
| 176 | + await user.type(screen.getByPlaceholderText('Message...'), 'Hello'); |
| 177 | + await user.keyboard('{Enter}'); |
| 178 | + |
| 179 | + const form = document.getElementById('input_form')!; |
| 180 | + const stopButton = await within(form).findByRole('button'); |
| 181 | + await user.click(stopButton); |
| 182 | + |
| 183 | + expect(capturedSignal?.aborted).toBe(true); |
| 184 | + }); |
| 185 | + |
| 186 | + test('aborting stream shows no error', async () => { |
| 187 | + const user = userEvent.setup(); |
| 188 | + vi.mocked(streamAnswer).mockImplementation(async function* () { |
| 189 | + const empty: Message[] = []; |
| 190 | + yield* empty; |
| 191 | + throw new DOMException('Aborted', 'AbortError'); |
| 192 | + }); |
| 193 | + |
| 194 | + renderChat(); |
| 195 | + await user.type(screen.getByPlaceholderText('Message...'), 'Hello'); |
| 196 | + await user.keyboard('{Enter}'); |
| 197 | + |
| 198 | + await waitFor(() => { |
| 199 | + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); |
| 200 | + expect(screen.getByRole('textbox')).not.toBeDisabled(); |
| 201 | + }); |
| 202 | + }); |
| 203 | + |
| 204 | + test('partial messages are preserved after stopping', async () => { |
| 205 | + const user = userEvent.setup(); |
| 206 | + |
| 207 | + vi.mocked(streamAnswer).mockImplementation(async function* (_c, _t, _a, _im, signal) { |
| 208 | + yield anAIMessage({ text: 'Partial response' }); |
| 209 | + await new Promise<void>((r) => signal.addEventListener('abort', () => r())); |
| 210 | + }); |
| 211 | + |
| 212 | + renderChat(); |
| 213 | + await user.type(screen.getByPlaceholderText('Message...'), 'Hello'); |
| 214 | + await user.keyboard('{Enter}'); |
| 215 | + |
| 216 | + await screen.findByText('Partial response'); |
| 217 | + |
| 218 | + const stopButton = within(document.getElementById('input_form')!).getByRole('button'); |
| 219 | + await user.click(stopButton); |
| 220 | + |
| 221 | + await waitFor(() => { |
| 222 | + expect(screen.getByText('Partial response')).toBeInTheDocument(); |
| 223 | + }); |
| 224 | + }); |
| 225 | + |
| 226 | + test('input is re-enabled after stopping', async () => { |
| 227 | + const user = userEvent.setup(); |
| 228 | + |
| 229 | + vi.mocked(streamAnswer).mockImplementation(async function* (_c, _t, _a, _im, signal) { |
| 230 | + yield anAIMessage({ text: 'Partial...' }); |
| 231 | + await new Promise<void>((r) => signal.addEventListener('abort', () => r())); |
| 232 | + }); |
| 233 | + |
| 234 | + renderChat(); |
| 235 | + await user.type(screen.getByPlaceholderText('Message...'), 'Hello'); |
| 236 | + await user.keyboard('{Enter}'); |
| 237 | + |
| 238 | + await waitFor(() => expect(screen.getByRole('textbox')).toBeDisabled()); |
| 239 | + |
| 240 | + await user.click(within(document.getElementById('input_form')!).getByRole('button')); |
| 241 | + |
| 242 | + await waitFor(() => expect(screen.getByRole('textbox')).not.toBeDisabled()); |
| 243 | + }); |
| 244 | + }); |
| 245 | + |
160 | 246 | describe('when rendered with existing thread messages', () => { |
161 | 247 | test('displays messages view immediately', async () => { |
162 | 248 | const existingMessages = [ |
|
0 commit comments