diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 4fe14fbea0..33c53b8e2f 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -5,7 +5,7 @@ */ import { renderWithProviders } from '../../test-utils/render.js'; -import { waitFor, act } from '@testing-library/react'; +import { act } from '@testing-library/react'; import type { InputPromptProps } from './InputPrompt.js'; import { InputPrompt } from './InputPrompt.js'; import type { TextBuffer } from './shared/text-buffer.js'; @@ -233,29 +233,29 @@ describe('InputPrompt', () => { }; }); - const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms)); - it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => { props.shellModeActive = true; const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\u001B[A'); - await wait(); - - expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => + expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled(), + ); unmount(); }); it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => { props.shellModeActive = true; const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\u001B[B'); - await wait(); - - expect(mockShellHistory.getNextCommand).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[B'); + }); + await vi.waitFor(() => + expect(mockShellHistory.getNextCommand).toHaveBeenCalled(), + ); unmount(); }); @@ -265,13 +265,14 @@ describe('InputPrompt', () => { 'previous command', ); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\u001B[A'); - await wait(); - - expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled(); - expect(props.buffer.setText).toHaveBeenCalledWith('previous command'); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => { + expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled(); + expect(props.buffer.setText).toHaveBeenCalledWith('previous command'); + }); unmount(); }); @@ -279,35 +280,47 @@ describe('InputPrompt', () => { props.shellModeActive = true; props.buffer.setText('ls -l'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\r'); - await wait(); - - expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith('ls -l'); - expect(props.onSubmit).toHaveBeenCalledWith('ls -l'); + await act(async () => { + stdin.write('\r'); + }); + await vi.waitFor(() => { + expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith( + 'ls -l', + ); + expect(props.onSubmit).toHaveBeenCalledWith('ls -l'); + }); unmount(); }); it('should NOT call shell history methods when not in shell mode', async () => { props.buffer.setText('some text'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\u001B[A'); // Up arrow - await wait(); - stdin.write('\u001B[B'); // Down arrow - await wait(); - stdin.write('\r'); // Enter - await wait(); + await act(async () => { + stdin.write('\u001B[A'); // Up arrow + }); + await vi.waitFor(() => + expect(mockInputHistory.navigateUp).toHaveBeenCalled(), + ); + + await act(async () => { + stdin.write('\u001B[B'); // Down arrow + }); + await vi.waitFor(() => + expect(mockInputHistory.navigateDown).toHaveBeenCalled(), + ); + + await act(async () => { + stdin.write('\r'); // Enter + }); + await vi.waitFor(() => + expect(props.onSubmit).toHaveBeenCalledWith('some text'), + ); expect(mockShellHistory.getPreviousCommand).not.toHaveBeenCalled(); expect(mockShellHistory.getNextCommand).not.toHaveBeenCalled(); expect(mockShellHistory.addCommandToHistory).not.toHaveBeenCalled(); - - expect(mockInputHistory.navigateUp).toHaveBeenCalled(); - expect(mockInputHistory.navigateDown).toHaveBeenCalled(); - expect(props.onSubmit).toHaveBeenCalledWith('some text'); unmount(); }); @@ -324,15 +337,21 @@ describe('InputPrompt', () => { props.buffer.setText('/mem'); const { stdin, unmount } = renderWithProviders(); - await wait(); // Test up arrow - stdin.write('\u001B[A'); // Up arrow - await wait(); + await act(async () => { + stdin.write('\u001B[A'); // Up arrow + }); + await vi.waitFor(() => + expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(1), + ); - stdin.write('\u0010'); // Ctrl+P - await wait(); - expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(2); + await act(async () => { + stdin.write('\u0010'); // Ctrl+P + }); + await vi.waitFor(() => + expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(2), + ); expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled(); unmount(); @@ -350,15 +369,21 @@ describe('InputPrompt', () => { props.buffer.setText('/mem'); const { stdin, unmount } = renderWithProviders(); - await wait(); // Test down arrow - stdin.write('\u001B[B'); // Down arrow - await wait(); + await act(async () => { + stdin.write('\u001B[B'); // Down arrow + }); + await vi.waitFor(() => + expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(1), + ); - stdin.write('\u000E'); // Ctrl+N - await wait(); - expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(2); + await act(async () => { + stdin.write('\u000E'); // Ctrl+N + }); + await vi.waitFor(() => + expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(2), + ); expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled(); unmount(); @@ -372,16 +397,27 @@ describe('InputPrompt', () => { props.buffer.setText('some text'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\u001B[A'); // Up arrow - await wait(); - stdin.write('\u001B[B'); // Down arrow - await wait(); - stdin.write('\u0010'); // Ctrl+P - await wait(); - stdin.write('\u000E'); // Ctrl+N - await wait(); + await act(async () => { + stdin.write('\u001B[A'); // Up arrow + }); + await vi.waitFor(() => + expect(mockInputHistory.navigateUp).toHaveBeenCalled(), + ); + await act(async () => { + stdin.write('\u001B[B'); // Down arrow + }); + await vi.waitFor(() => + expect(mockInputHistory.navigateDown).toHaveBeenCalled(), + ); + await act(async () => { + stdin.write('\u0010'); // Ctrl+P + }); + await vi.waitFor(() => {}); + await act(async () => { + stdin.write('\u000E'); // Ctrl+N + }); + await vi.waitFor(() => {}); expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled(); expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled(); @@ -406,20 +442,21 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); // Send Ctrl+V - stdin.write('\x16'); // Ctrl+V - await wait(); - - expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled(); - expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith( - props.config.getTargetDir(), - ); - expect(clipboardUtils.cleanupOldClipboardImages).toHaveBeenCalledWith( - props.config.getTargetDir(), - ); - expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled(); + await act(async () => { + stdin.write('\x16'); // Ctrl+V + }); + await vi.waitFor(() => { + expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled(); + expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith( + props.config.getTargetDir(), + ); + expect(clipboardUtils.cleanupOldClipboardImages).toHaveBeenCalledWith( + props.config.getTargetDir(), + ); + expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled(); + }); unmount(); }); @@ -429,12 +466,13 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x16'); // Ctrl+V - await wait(); - - expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled(); + await act(async () => { + stdin.write('\x16'); // Ctrl+V + }); + await vi.waitFor(() => { + expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled(); + }); expect(clipboardUtils.saveClipboardImage).not.toHaveBeenCalled(); expect(mockBuffer.setText).not.toHaveBeenCalled(); unmount(); @@ -447,12 +485,13 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x16'); // Ctrl+V - await wait(); - - expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled(); + await act(async () => { + stdin.write('\x16'); // Ctrl+V + }); + await vi.waitFor(() => { + expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled(); + }); expect(mockBuffer.setText).not.toHaveBeenCalled(); unmount(); }); @@ -475,13 +514,14 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x16'); // Ctrl+V - await wait(); - - // Should insert at cursor position with spaces - expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled(); + await act(async () => { + stdin.write('\x16'); // Ctrl+V + }); + await vi.waitFor(() => { + // Should insert at cursor position with spaces + expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled(); + }); // Get the actual call to see what path was used const actualCall = vi.mocked(mockBuffer.replaceRangeByOffset).mock @@ -505,15 +545,16 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x16'); // Ctrl+V - await wait(); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error handling clipboard image:', - expect.any(Error), - ); + await act(async () => { + stdin.write('\x16'); // Ctrl+V + }); + await vi.waitFor(() => { + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Error handling clipboard image:', + expect.any(Error), + ); + }); expect(mockBuffer.setText).not.toHaveBeenCalled(); consoleErrorSpy.mockRestore(); @@ -532,12 +573,13 @@ describe('InputPrompt', () => { props.buffer.setText('/mem'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\t'); // Press Tab - await wait(); - - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + await act(async () => { + stdin.write('\t'); // Press Tab + }); + await vi.waitFor(() => + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), + ); unmount(); }); @@ -555,12 +597,13 @@ describe('InputPrompt', () => { props.buffer.setText('/memory '); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\t'); // Press Tab - await wait(); - - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1); + await act(async () => { + stdin.write('\t'); // Press Tab + }); + await vi.waitFor(() => + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1), + ); unmount(); }); @@ -579,13 +622,14 @@ describe('InputPrompt', () => { props.buffer.setText('/memory'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\t'); // Press Tab - await wait(); - - // It should NOT become '/show'. It should correctly become '/memory show'. - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + await act(async () => { + stdin.write('\t'); // Press Tab + }); + await vi.waitFor(() => + // It should NOT become '/show'. It should correctly become '/memory show'. + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), + ); unmount(); }); @@ -600,12 +644,13 @@ describe('InputPrompt', () => { props.buffer.setText('/chat resume fi-'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\t'); // Press Tab - await wait(); - - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + await act(async () => { + stdin.write('\t'); // Press Tab + }); + await vi.waitFor(() => + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), + ); unmount(); }); @@ -619,13 +664,14 @@ describe('InputPrompt', () => { props.buffer.setText('/mem'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\r'); - await wait(); - - // The app should autocomplete the text, NOT submit. - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + await act(async () => { + stdin.write('\r'); + }); + await vi.waitFor(() => { + // The app should autocomplete the text, NOT submit. + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + }); expect(props.onSubmit).not.toHaveBeenCalled(); unmount(); @@ -650,12 +696,13 @@ describe('InputPrompt', () => { props.buffer.setText('/?'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\t'); // Press Tab for autocomplete - await wait(); - - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + await act(async () => { + stdin.write('\t'); // Press Tab for autocomplete + }); + await vi.waitFor(() => + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), + ); unmount(); }); @@ -663,10 +710,11 @@ describe('InputPrompt', () => { props.buffer.setText(' '); // Set buffer to whitespace const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\r'); // Press Enter - await wait(); + await act(async () => { + stdin.write('\r'); // Press Enter + }); + await vi.waitFor(() => {}); expect(props.onSubmit).not.toHaveBeenCalled(); unmount(); @@ -681,12 +729,13 @@ describe('InputPrompt', () => { props.buffer.setText('/clear'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\r'); - await wait(); - - expect(props.onSubmit).toHaveBeenCalledWith('/clear'); + await act(async () => { + stdin.write('\r'); + }); + await vi.waitFor(() => + expect(props.onSubmit).toHaveBeenCalledWith('/clear'), + ); unmount(); }); @@ -699,12 +748,13 @@ describe('InputPrompt', () => { props.buffer.setText('/clear'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\r'); - await wait(); - - expect(props.onSubmit).toHaveBeenCalledWith('/clear'); + await act(async () => { + stdin.write('\r'); + }); + await vi.waitFor(() => + expect(props.onSubmit).toHaveBeenCalledWith('/clear'), + ); unmount(); }); @@ -718,12 +768,13 @@ describe('InputPrompt', () => { props.buffer.setText('@src/components/'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\r'); - await wait(); - - expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0); + await act(async () => { + stdin.write('\r'); + }); + await vi.waitFor(() => + expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0), + ); expect(props.onSubmit).not.toHaveBeenCalled(); unmount(); }); @@ -735,27 +786,30 @@ describe('InputPrompt', () => { mockBuffer.lines = ['first line\\']; const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\r'); - await wait(); + await act(async () => { + stdin.write('\r'); + }); + await vi.waitFor(() => { + expect(props.buffer.backspace).toHaveBeenCalled(); + expect(props.buffer.newline).toHaveBeenCalled(); + }); expect(props.onSubmit).not.toHaveBeenCalled(); - expect(props.buffer.backspace).toHaveBeenCalled(); - expect(props.buffer.newline).toHaveBeenCalled(); unmount(); }); it('should clear the buffer on Ctrl+C if it has text', async () => { props.buffer.setText('some text to clear'); const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\x03'); // Ctrl+C character - await wait(60); - - expect(props.buffer.setText).toHaveBeenCalledWith(''); - expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(); + await act(async () => { + stdin.write('\x03'); // Ctrl+C character + }); + await vi.waitFor(() => { + expect(props.buffer.setText).toHaveBeenCalledWith(''); + expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(); + }); expect(props.onSubmit).not.toHaveBeenCalled(); unmount(); }); @@ -763,10 +817,11 @@ describe('InputPrompt', () => { it('should NOT clear the buffer on Ctrl+C if it is empty', async () => { props.buffer.text = ''; const { stdin, unmount } = renderWithProviders(); - await wait(); - stdin.write('\x03'); // Ctrl+C character - await wait(60); + await act(async () => { + stdin.write('\x03'); // Ctrl+C character + }); + await vi.waitFor(() => {}); expect(props.buffer.setText).not.toHaveBeenCalled(); unmount(); @@ -866,18 +921,19 @@ describe('InputPrompt', () => { }); const { unmount } = renderWithProviders(); - await wait(); - expect(mockedUseCommandCompletion).toHaveBeenCalledWith( - mockBuffer, - ['/test/project/src'], - path.join('test', 'project', 'src'), - mockSlashCommands, - mockCommandContext, - false, - false, - expect.any(Object), - ); + await vi.waitFor(() => { + expect(mockedUseCommandCompletion).toHaveBeenCalledWith( + mockBuffer, + ['/test/project/src'], + path.join('test', 'project', 'src'), + mockSlashCommands, + mockCommandContext, + false, + false, + expect.any(Object), + ); + }); unmount(); }); @@ -889,12 +945,13 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('i'); - await wait(); - - expect(props.vimHandleInput).toHaveBeenCalled(); + await act(async () => { + stdin.write('i'); + }); + await vi.waitFor(() => { + expect(props.vimHandleInput).toHaveBeenCalled(); + }); expect(mockBuffer.handleInput).not.toHaveBeenCalled(); unmount(); }); @@ -904,13 +961,14 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('i'); - await wait(); - - expect(props.vimHandleInput).toHaveBeenCalled(); - expect(mockBuffer.handleInput).toHaveBeenCalled(); + await act(async () => { + stdin.write('i'); + }); + await vi.waitFor(() => { + expect(props.vimHandleInput).toHaveBeenCalled(); + expect(mockBuffer.handleInput).toHaveBeenCalled(); + }); unmount(); }); @@ -920,13 +978,14 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('i'); - await wait(); - - expect(props.vimHandleInput).toHaveBeenCalled(); - expect(mockBuffer.handleInput).toHaveBeenCalled(); + await act(async () => { + stdin.write('i'); + }); + await vi.waitFor(() => { + expect(props.vimHandleInput).toHaveBeenCalled(); + expect(mockBuffer.handleInput).toHaveBeenCalled(); + }); unmount(); }); }); @@ -937,17 +996,18 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x1B[200~pasted text\x1B[201~'); - await wait(60); - - expect(mockBuffer.handleInput).toHaveBeenCalledWith( - expect.objectContaining({ - paste: true, - sequence: 'pasted text', - }), - ); + await act(async () => { + stdin.write('\x1B[200~pasted text\x1B[201~'); + }); + await vi.waitFor(() => { + expect(mockBuffer.handleInput).toHaveBeenCalledWith( + expect.objectContaining({ + paste: true, + sequence: 'pasted text', + }), + ); + }); unmount(); }); @@ -956,10 +1016,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('a'); - await wait(); + await act(async () => { + stdin.write('a'); + }); + await vi.waitFor(() => {}); expect(mockBuffer.handleInput).not.toHaveBeenCalled(); unmount(); @@ -1028,10 +1089,11 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - const frame = stdout.lastFrame(); - expect(frame).toContain(expected); + await vi.waitFor(() => { + const frame = stdout.lastFrame(); + expect(frame).toContain(expected); + }); unmount(); }, ); @@ -1084,10 +1146,11 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - const frame = stdout.lastFrame(); - expect(frame).toContain(expected); + await vi.waitFor(() => { + const frame = stdout.lastFrame(); + expect(frame).toContain(expected); + }); unmount(); }, ); @@ -1107,14 +1170,15 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - const frame = stdout.lastFrame(); - const lines = frame!.split('\n'); - // The line with the cursor should just be an inverted space inside the box border - expect( - lines.find((l) => l.includes(chalk.inverse(' '))), - ).not.toBeUndefined(); + await vi.waitFor(() => { + const frame = stdout.lastFrame(); + const lines = frame!.split('\n'); + // The line with the cursor should just be an inverted space inside the box border + expect( + lines.find((l) => l.includes(chalk.inverse(' '))), + ).not.toBeUndefined(); + }); unmount(); }); }); @@ -1138,17 +1202,18 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - const frame = stdout.lastFrame(); - // Check that all lines, including the empty one, are rendered. - // This implicitly tests that the Box wrapper provides height for the empty line. - expect(frame).toContain('hello'); - expect(frame).toContain(`world${chalk.inverse(' ')}`); + await vi.waitFor(() => { + const frame = stdout.lastFrame(); + // Check that all lines, including the empty one, are rendered. + // This implicitly tests that the Box wrapper provides height for the empty line. + expect(frame).toContain('hello'); + expect(frame).toContain(`world${chalk.inverse(' ')}`); - const outputLines = frame!.split('\n'); - // The number of lines should be 2 for the border plus 3 for the content. - expect(outputLines.length).toBe(5); + const outputLines = frame!.split('\n'); + // The number of lines should be 2 for the border plus 3 for the content. + expect(outputLines.length).toBe(5); + }); unmount(); }); }); @@ -1171,20 +1236,21 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); // Simulate a bracketed paste event from the terminal - stdin.write(`\x1b[200~${pastedText}\x1b[201~`); - await wait(); - - // Verify that the buffer's handleInput was called once with the full text - expect(props.buffer.handleInput).toHaveBeenCalledTimes(1); - expect(props.buffer.handleInput).toHaveBeenCalledWith( - expect.objectContaining({ - paste: true, - sequence: pastedText, - }), - ); + await act(async () => { + stdin.write(`\x1b[200~${pastedText}\x1b[201~`); + }); + await vi.waitFor(() => { + // Verify that the buffer's handleInput was called once with the full text + expect(props.buffer.handleInput).toHaveBeenCalledTimes(1); + expect(props.buffer.handleInput).toHaveBeenCalledWith( + expect.objectContaining({ + paste: true, + sequence: pastedText, + }), + ); + }); unmount(); }); @@ -1214,12 +1280,14 @@ describe('InputPrompt', () => { await vi.runAllTimersAsync(); // Simulate a paste operation (this should set the paste protection) - act(() => { + await act(async () => { stdin.write(`\x1b[200~pasted content\x1b[201~`); }); // Simulate an Enter key press immediately after paste - stdin.write('\r'); + await act(async () => { + stdin.write('\r'); + }); await vi.runAllTimersAsync(); // Verify that onSubmit was NOT called due to recent paste protection @@ -1239,7 +1307,7 @@ describe('InputPrompt', () => { await vi.runAllTimersAsync(); // Simulate a paste operation (this sets the protection) - act(() => { + await act(async () => { stdin.write('\x1b[200~pasted text\x1b[201~'); }); await vi.runAllTimersAsync(); @@ -1250,7 +1318,9 @@ describe('InputPrompt', () => { }); // Now Enter should work normally - stdin.write('\r'); + await act(async () => { + stdin.write('\r'); + }); await vi.runAllTimersAsync(); expect(props.onSubmit).toHaveBeenCalledWith('pasted text'); @@ -1282,11 +1352,15 @@ describe('InputPrompt', () => { await vi.runAllTimersAsync(); // Simulate a paste operation - stdin.write('\x1b[200~some pasted stuff\x1b[201~'); + await act(async () => { + stdin.write('\x1b[200~some pasted stuff\x1b[201~'); + }); await vi.runAllTimersAsync(); // Simulate an Enter key press immediately after paste - stdin.write('\r'); + await act(async () => { + stdin.write('\r'); + }); await vi.runAllTimersAsync(); // Verify that onSubmit was called @@ -1305,7 +1379,9 @@ describe('InputPrompt', () => { await vi.runAllTimersAsync(); // Press Enter without any recent paste - stdin.write('\r'); + await act(async () => { + stdin.write('\r'); + }); await vi.runAllTimersAsync(); // Verify that onSubmit was called normally @@ -1325,16 +1401,21 @@ describe('InputPrompt', () => { , { kittyProtocolEnabled: false }, ); - await wait(); - stdin.write('\x1B'); - await wait(); + await act(async () => { + stdin.write('\x1B'); + }); + await vi.waitFor(() => { + expect(onEscapePromptChange).toHaveBeenCalledWith(true); + }); - stdin.write('\x1B'); - await wait(60); - - expect(props.buffer.setText).toHaveBeenCalledWith(''); - expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(); + await act(async () => { + stdin.write('\x1B'); + }); + await vi.waitFor(() => { + expect(props.buffer.setText).toHaveBeenCalledWith(''); + expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(); + }); unmount(); }); @@ -1348,15 +1429,19 @@ describe('InputPrompt', () => { { kittyProtocolEnabled: false }, ); - stdin.write('\x1B'); + await act(async () => { + stdin.write('\x1B'); + }); - await waitFor(() => { + await vi.waitFor(() => { expect(onEscapePromptChange).toHaveBeenCalledWith(true); }); - stdin.write('a'); + await act(async () => { + stdin.write('a'); + }); - await waitFor(() => { + await vi.waitFor(() => { expect(onEscapePromptChange).toHaveBeenCalledWith(false); }); unmount(); @@ -1369,12 +1454,13 @@ describe('InputPrompt', () => { , { kittyProtocolEnabled: false }, ); - await wait(); - stdin.write('\x1B'); - await wait(100); - - expect(props.setShellModeActive).toHaveBeenCalledWith(false); + await act(async () => { + stdin.write('\x1B'); + }); + await vi.waitFor(() => + expect(props.setShellModeActive).toHaveBeenCalledWith(false), + ); unmount(); }); @@ -1389,12 +1475,13 @@ describe('InputPrompt', () => { , { kittyProtocolEnabled: false }, ); - await wait(); - stdin.write('\x1B'); - await wait(60); - - expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(); + await act(async () => { + stdin.write('\x1B'); + }); + await vi.waitFor(() => + expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(), + ); unmount(); }); @@ -1409,7 +1496,9 @@ describe('InputPrompt', () => { ); await vi.runAllTimersAsync(); - stdin.write('\x1B'); + await act(async () => { + stdin.write('\x1B'); + }); await vi.runAllTimersAsync(); vi.useRealTimers(); @@ -1421,17 +1510,18 @@ describe('InputPrompt', () => { , { kittyProtocolEnabled: false }, ); - await wait(); - stdin.write('\x0C'); - await wait(); + await act(async () => { + stdin.write('\x0C'); + }); + await vi.waitFor(() => expect(props.onClearScreen).toHaveBeenCalled()); - expect(props.onClearScreen).toHaveBeenCalled(); - - stdin.write('\x01'); - await wait(); - - expect(props.buffer.move).toHaveBeenCalledWith('home'); + await act(async () => { + stdin.write('\x01'); + }); + await vi.waitFor(() => + expect(props.buffer.move).toHaveBeenCalledWith('home'), + ); unmount(); }); }); @@ -1465,14 +1555,13 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - await wait(); // Trigger reverse search with Ctrl+R - act(() => { + await act(async () => { stdin.write('\x12'); }); - await waitFor(() => { + await vi.waitFor(() => { const frame = stdout.lastFrame(); expect(frame).toContain('(r:)'); expect(frame).toContain('echo hello'); @@ -1487,14 +1576,19 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x12'); - await wait(); - stdin.write('\x1B'); - stdin.write('\u001b[27u'); // Press kitty escape key + await act(async () => { + stdin.write('\x12'); + }); + await vi.waitFor(() => {}); + await act(async () => { + stdin.write('\x1B'); + }); + await act(async () => { + stdin.write('\u001b[27u'); // Press kitty escape key + }); - await waitFor(() => { + await vi.waitFor(() => { expect(stdout.lastFrame()).not.toContain('(r:)'); }); @@ -1530,23 +1624,23 @@ describe('InputPrompt', () => { ); // Enter reverse search mode with Ctrl+R - act(() => { + await act(async () => { stdin.write('\x12'); }); // Verify reverse search is active - await waitFor(() => { + await vi.waitFor(() => { expect(stdout.lastFrame()).toContain('(r:)'); }); // Press Tab to complete the highlighted entry - act(() => { + await act(async () => { stdin.write('\t'); }); - await wait(); - - expect(mockHandleAutocomplete).toHaveBeenCalledWith(0); - expect(props.buffer.setText).toHaveBeenCalledWith('echo hello'); + await vi.waitFor(() => { + expect(mockHandleAutocomplete).toHaveBeenCalledWith(0); + expect(props.buffer.setText).toHaveBeenCalledWith('echo hello'); + }); unmount(); }, 15000); @@ -1567,19 +1661,19 @@ describe('InputPrompt', () => { , ); - act(() => { + await act(async () => { stdin.write('\x12'); }); - await waitFor(() => { + await vi.waitFor(() => { expect(stdout.lastFrame()).toContain('(r:)'); }); - act(() => { + await act(async () => { stdin.write('\r'); }); - await waitFor(() => { + await vi.waitFor(() => { expect(stdout.lastFrame()).not.toContain('(r:)'); }); @@ -1608,23 +1702,22 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - await wait(); // reverse search with Ctrl+R - act(() => { + await act(async () => { stdin.write('\x12'); }); - await waitFor(() => { + await vi.waitFor(() => { expect(stdout.lastFrame()).toContain('(r:)'); }); // Press kitty escape key - act(() => { + await act(async () => { stdin.write('\u001b[27u'); }); - await waitFor(() => { + await vi.waitFor(() => { expect(stdout.lastFrame()).not.toContain('(r:)'); expect(props.buffer.text).toBe(initialText); expect(props.buffer.cursor).toEqual(initialCursor); @@ -1643,12 +1736,13 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x05'); // Ctrl+E - await wait(); - - expect(props.buffer.move).toHaveBeenCalledWith('end'); + await act(async () => { + stdin.write('\x05'); // Ctrl+E + }); + await vi.waitFor(() => { + expect(props.buffer.move).toHaveBeenCalledWith('end'); + }); expect(props.buffer.moveToOffset).not.toHaveBeenCalled(); unmount(); }); @@ -1661,12 +1755,13 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x05'); // Ctrl+E - await wait(); - - expect(props.buffer.move).toHaveBeenCalledWith('end'); + await act(async () => { + stdin.write('\x05'); // Ctrl+E + }); + await vi.waitFor(() => { + expect(props.buffer.move).toHaveBeenCalledWith('end'); + }); expect(props.buffer.moveToOffset).not.toHaveBeenCalled(); unmount(); }); @@ -1693,17 +1788,17 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - await wait(); - act(() => { + await act(async () => { stdin.write('\x12'); // Ctrl+R }); - await wait(); - const frame = stdout.lastFrame() ?? ''; - expect(frame).toContain('(r:)'); - expect(frame).toContain('git commit'); - expect(frame).toContain('git push'); + await vi.waitFor(() => { + const frame = stdout.lastFrame() ?? ''; + expect(frame).toContain('(r:)'); + expect(frame).toContain('git commit'); + expect(frame).toContain('git push'); + }); unmount(); }); @@ -1723,25 +1818,32 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x12'); - await wait(); + await act(async () => { + stdin.write('\x12'); + }); + await vi.waitFor(() => { + expect(clean(stdout.lastFrame())).toContain('→'); + }); - expect(clean(stdout.lastFrame())).toContain('→'); - - stdin.write('\u001B[C'); - await wait(200); - expect(clean(stdout.lastFrame())).toContain('←'); + await act(async () => { + stdin.write('\u001B[C'); + }); + await vi.waitFor(() => { + expect(clean(stdout.lastFrame())).toContain('←'); + }); expect(stdout.lastFrame()).toMatchSnapshot( - 'command-search-expanded-match', + 'command-search-render-expanded-match', ); - stdin.write('\u001B[D'); - await wait(); - expect(clean(stdout.lastFrame())).toContain('→'); + await act(async () => { + stdin.write('\u001B[D'); + }); + await vi.waitFor(() => { + expect(clean(stdout.lastFrame())).toContain('→'); + }); expect(stdout.lastFrame()).toMatchSnapshot( - 'command-search-collapsed-match', + 'command-search-render-collapsed-match', ); unmount(); }); @@ -1765,19 +1867,24 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x12'); - await wait(); - expect(stdout.lastFrame()).toMatchSnapshot( - 'command-search-collapsed-match', - ); + await act(async () => { + stdin.write('\x12'); + }); + await vi.waitFor(() => { + expect(stdout.lastFrame()).toMatchSnapshot( + 'command-search-render-collapsed-match', + ); + }); - stdin.write('\u001B[C'); - await wait(); - expect(stdout.lastFrame()).toMatchSnapshot( - 'command-search-expanded-match', - ); + await act(async () => { + stdin.write('\u001B[C'); + }); + await vi.waitFor(() => { + expect(stdout.lastFrame()).toMatchSnapshot( + 'command-search-render-expanded-match', + ); + }); unmount(); }); @@ -1798,14 +1905,17 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\x12'); - await wait(); - - const frame = clean(stdout.lastFrame()); - expect(frame).not.toContain('→'); - expect(frame).not.toContain('←'); + await act(async () => { + stdin.write('\x12'); + }); + await vi.waitFor(() => { + const frame = clean(stdout.lastFrame()); + // Ensure it rendered the search mode + expect(frame).toContain('(r:)'); + expect(frame).not.toContain('→'); + expect(frame).not.toContain('←'); + }); unmount(); }); }); @@ -1819,12 +1929,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); - - expect(mockPopAllMessages).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled()); const callback = mockPopAllMessages.mock.calls[0][0]; act(() => { @@ -1844,12 +1953,14 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => + expect(mockInputHistory.navigateUp).toHaveBeenCalled(), + ); expect(mockPopAllMessages).not.toHaveBeenCalled(); - expect(mockInputHistory.navigateUp).toHaveBeenCalled(); unmount(); }); @@ -1861,12 +1972,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); - - expect(mockPopAllMessages).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled()); const callback = mockPopAllMessages.mock.calls[0][0]; act(() => { callback(undefined); @@ -1888,11 +1998,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); - expect(mockPopAllMessages).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled()); unmount(); }); @@ -1904,10 +2014,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled()); const callback = mockPopAllMessages.mock.calls[0][0]; act(() => { @@ -1926,12 +2037,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); - - expect(mockPopAllMessages).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled()); unmount(); }); @@ -1942,12 +2052,13 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); - - expect(mockInputHistory.navigateUp).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => + expect(mockInputHistory.navigateUp).toHaveBeenCalled(), + ); unmount(); }); @@ -1959,12 +2070,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\u001B[A'); - await wait(); - - expect(mockPopAllMessages).toHaveBeenCalled(); + await act(async () => { + stdin.write('\u001B[A'); + }); + await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled()); const callback = mockPopAllMessages.mock.calls[0][0]; act(() => { @@ -1984,8 +2094,7 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - expect(stdout.lastFrame()).toMatchSnapshot(); + await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot()); unmount(); }); @@ -1994,8 +2103,7 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - expect(stdout.lastFrame()).toMatchSnapshot(); + await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot()); unmount(); }); @@ -2004,8 +2112,7 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - expect(stdout.lastFrame()).toMatchSnapshot(); + await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot()); unmount(); }); @@ -2015,11 +2122,12 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders( , ); - await wait(); - expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`); - // This snapshot is good to make sure there was an input prompt but does - // not show the inverted cursor because snapshots do not show colors. - expect(stdout.lastFrame()).toMatchSnapshot(); + await vi.waitFor(() => { + expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`); + // This snapshot is good to make sure there was an input prompt but does + // not show the inverted cursor because snapshots do not show colors. + expect(stdout.lastFrame()).toMatchSnapshot(); + }); unmount(); }); }); @@ -2028,12 +2136,11 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders(, { shellFocus: false, }); - await wait(); - stdin.write('a'); - await wait(); - - expect(mockBuffer.handleInput).toHaveBeenCalled(); + await act(async () => { + stdin.write('a'); + }); + await vi.waitFor(() => expect(mockBuffer.handleInput).toHaveBeenCalled()); unmount(); }); describe('command queuing while streaming', () => { @@ -2074,17 +2181,20 @@ describe('InputPrompt', () => { const { stdin, unmount } = renderWithProviders( , ); - await wait(); - stdin.write('\r'); - await wait(); - - if (shouldSubmit) { - expect(props.onSubmit).toHaveBeenCalledWith(bufferText); - expect(props.setQueueErrorMessage).not.toHaveBeenCalled(); - } else { - expect(props.onSubmit).not.toHaveBeenCalled(); - expect(props.setQueueErrorMessage).toHaveBeenCalledWith(errorMessage); - } + await act(async () => { + stdin.write('\r'); + }); + await vi.waitFor(() => { + if (shouldSubmit) { + expect(props.onSubmit).toHaveBeenCalledWith(bufferText); + expect(props.setQueueErrorMessage).not.toHaveBeenCalled(); + } else { + expect(props.onSubmit).not.toHaveBeenCalled(); + expect(props.setQueueErrorMessage).toHaveBeenCalledWith( + errorMessage, + ); + } + }); unmount(); }, ); diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap index 5ce22ac941..4991f1ac4f 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap @@ -18,14 +18,44 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c llllllllllllllllllllllllllllllllllllllllllllllllll" `; -exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-collapsed-match 1`] = ` +exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = ` +"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ (r:) Type your message or @path/to/file │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll → + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll + ..." +`; + +exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-expanded-match 1`] = ` +"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ (r:) Type your message or @path/to/file │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ← + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll + llllllllllllllllllllllllllllllllllllllllllllllllll" +`; + +exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = ` +"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ > commit │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 2`] = ` "╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ (r:) commit │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ git commit -m "feat: add search" in src/app" `; -exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-expanded-match 1`] = ` +exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = ` +"╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ > commit │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 2`] = ` "╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ (r:) commit │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯