Download packages/cli/src/ui/components/AskUserDialog.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 46.7 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/AskUserDialog.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/components/AskUserDialog.test.tsx
-
curl -L -o AskUserDialog.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/AskUserDialog.test.tsx
46.7 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; | |
| import { act } from 'react'; | |
| import { renderWithProviders } from '../../test-utils/render.js'; | |
| import { createMockSettings } from '../../test-utils/settings.js'; | |
| import { makeFakeConfig } from '@google/gemini-cli-core'; | |
| import { waitFor } from '../../test-utils/async.js'; | |
| import { AskUserDialog } from './AskUserDialog.js'; | |
| import { QuestionType, type Question } from '@google/gemini-cli-core'; | |
| import { UIStateContext, type UIState } from '../contexts/UIStateContext.js'; | |
| // Helper to write to stdin with proper act() wrapping | |
| const writeKey = (stdin: { write: (data: string) => void }, key: string) => { | |
| act(() => { | |
| stdin.write(key); | |
| }); | |
| }; | |
| describe('AskUserDialog', () => { | |
| // Ensure keystrokes appear spaced in time to avoid bufferFastReturn | |
| // converting Enter into Shift+Enter during synchronous test execution. | |
| let mockTime: number; | |
| beforeEach(() => { | |
| mockTime = 0; | |
| vi.spyOn(Date, 'now').mockImplementation(() => (mockTime += 50)); | |
| }); | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| }); | |
| const authQuestion: Question[] = [ | |
| { | |
| question: 'Which authentication method should we use?', | |
| header: 'Auth', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'OAuth 2.0', description: 'Industry standard, supports SSO' }, | |
| { label: 'JWT tokens', description: 'Stateless, good for APIs' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| it('renders question and options', async () => { | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={authQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| describe.each([ | |
| { | |
| name: 'Single Select', | |
| questions: authQuestion, | |
| actions: (stdin: { write: (data: string) => void }) => { | |
| writeKey(stdin, '\r'); | |
| }, | |
| expectedSubmit: { '0': 'OAuth 2.0' }, | |
| }, | |
| { | |
| name: 'Multi-select', | |
| questions: [ | |
| { | |
| question: 'Which features?', | |
| header: 'Features', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'TypeScript', description: '' }, | |
| { label: 'ESLint', description: '' }, | |
| ], | |
| multiSelect: true, | |
| }, | |
| ] as Question[], | |
| actions: (stdin: { write: (data: string) => void }) => { | |
| writeKey(stdin, '\r'); // Toggle TS | |
| writeKey(stdin, '\x1b[B'); // Down | |
| writeKey(stdin, '\r'); // Toggle ESLint | |
| writeKey(stdin, '\x1b[B'); // Down to All of the above | |
| writeKey(stdin, '\x1b[B'); // Down to Other | |
| writeKey(stdin, '\x1b[B'); // Down to Done | |
| writeKey(stdin, '\r'); // Done | |
| }, | |
| expectedSubmit: { '0': 'TypeScript, ESLint' }, | |
| }, | |
| { | |
| name: 'All of the above', | |
| questions: [ | |
| { | |
| question: 'Which features?', | |
| header: 'Features', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'TypeScript', description: '' }, | |
| { label: 'ESLint', description: '' }, | |
| ], | |
| multiSelect: true, | |
| }, | |
| ] as Question[], | |
| actions: (stdin: { write: (data: string) => void }) => { | |
| writeKey(stdin, '\x1b[B'); // Down to ESLint | |
| writeKey(stdin, '\x1b[B'); // Down to All of the above | |
| writeKey(stdin, '\r'); // Toggle All of the above | |
| writeKey(stdin, '\x1b[B'); // Down to Other | |
| writeKey(stdin, '\x1b[B'); // Down to Done | |
| writeKey(stdin, '\r'); // Done | |
| }, | |
| expectedSubmit: { '0': 'TypeScript, ESLint' }, | |
| }, | |
| { | |
| name: 'Text Input', | |
| questions: [ | |
| { | |
| question: 'Name?', | |
| header: 'Name', | |
| type: QuestionType.TEXT, | |
| }, | |
| ] as Question[], | |
| actions: (stdin: { write: (data: string) => void }) => { | |
| for (const char of 'test-app') { | |
| writeKey(stdin, char); | |
| } | |
| writeKey(stdin, '\r'); | |
| }, | |
| expectedSubmit: { '0': 'test-app' }, | |
| }, | |
| ])('Submission: $name', ({ name, questions, actions, expectedSubmit }) => { | |
| it(`submits correct values for ${name}`, async () => { | |
| const onSubmit = vi.fn(); | |
| const { stdin } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| actions(stdin); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith(expectedSubmit); | |
| }); | |
| }); | |
| }); | |
| it('verifies "All of the above" visual state with snapshot', async () => { | |
| const questions = [ | |
| { | |
| question: 'Which features?', | |
| header: 'Features', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'TypeScript', description: '' }, | |
| { label: 'ESLint', description: '' }, | |
| ], | |
| multiSelect: true, | |
| }, | |
| ] as Question[]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Navigate to "All of the above" and toggle it | |
| writeKey(stdin, '\x1b[B'); // Down to ESLint | |
| writeKey(stdin, '\x1b[B'); // Down to All of the above | |
| writeKey(stdin, '\r'); // Toggle All of the above | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| // Verify visual state (checkmarks on all options) | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| }); | |
| it('handles custom option in single select with inline typing', async () => { | |
| const onSubmit = vi.fn(); | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={authQuestion} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Move down to custom option | |
| writeKey(stdin, '\x1b[B'); | |
| writeKey(stdin, '\x1b[B'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Enter a custom value'); | |
| }); | |
| // Type directly (inline) | |
| for (const char of 'API Key') { | |
| writeKey(stdin, char); | |
| } | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('API Key'); | |
| }); | |
| // Press Enter to submit the custom value | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({ '0': 'API Key' }); | |
| }); | |
| }); | |
| it('supports multi-line input for "Other" option in choice questions', async () => { | |
| const authQuestionWithOther: Question[] = [ | |
| { | |
| question: 'Which authentication method?', | |
| header: 'Auth', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'OAuth 2.0', description: '' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={authQuestionWithOther} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Navigate to "Other" option | |
| writeKey(stdin, '\x1b[B'); // Down to "Other" | |
| // Type first line | |
| for (const char of 'Line 1') { | |
| writeKey(stdin, char); | |
| } | |
| // Insert newline using \ + Enter (handled by bufferBackslashEnter) | |
| writeKey(stdin, '\\'); | |
| writeKey(stdin, '\r'); | |
| // Type second line | |
| for (const char of 'Line 2') { | |
| writeKey(stdin, char); | |
| } | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Line 1'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Line 2'); | |
| }); | |
| // Press Enter to submit | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({ '0': 'Line 1\nLine 2' }); | |
| }); | |
| }); | |
| describe.each([ | |
| { useAlternateBuffer: true, expectedArrows: true }, | |
| { useAlternateBuffer: false, expectedArrows: true }, | |
| ])( | |
| 'Scroll Arrows (useAlternateBuffer: $useAlternateBuffer)', | |
| ({ useAlternateBuffer, expectedArrows }) => { | |
| it(`shows scroll arrows correctly when useAlternateBuffer is ${useAlternateBuffer}`, async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Choose an option', | |
| header: 'Scroll Test', | |
| type: QuestionType.CHOICE, | |
| options: Array.from({ length: 15 }, (_, i) => ({ | |
| label: `Option ${i + 1}`, | |
| description: `Description ${i + 1}`, | |
| })), | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={80} | |
| availableHeight={10} // Small height to force scrolling | |
| />, | |
| { | |
| config: makeFakeConfig({ useAlternateBuffer }), | |
| settings: createMockSettings({ ui: { useAlternateBuffer } }), | |
| }, | |
| ); | |
| await waitFor(async () => { | |
| if (expectedArrows) { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('▲'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('▼'); | |
| } else { | |
| await waitUntilReady(); | |
| expect(lastFrame()).not.toContain('▲'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).not.toContain('▼'); | |
| } | |
| await waitUntilReady(); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| }); | |
| }, | |
| ); | |
| it('navigates to custom option when typing unbound characters (Type-to-Jump)', async () => { | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={authQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Type a character without navigating down | |
| writeKey(stdin, 'A'); | |
| await waitFor(async () => { | |
| // Should show the custom input with 'A' | |
| // Placeholder is hidden when text is present | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('A'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('3. A'); | |
| }); | |
| // Continue typing | |
| writeKey(stdin, 'P'); | |
| writeKey(stdin, 'I'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('API'); | |
| }); | |
| }); | |
| it('shows progress header for multiple questions', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Which database should we use?', | |
| header: 'Database', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'PostgreSQL', description: 'Relational database' }, | |
| { label: 'MongoDB', description: 'Document database' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Which ORM do you prefer?', | |
| header: 'ORM', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'Prisma', description: 'Type-safe ORM' }, | |
| { label: 'Drizzle', description: 'Lightweight ORM' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| it('hides progress header for single question', async () => { | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={authQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| it('shows keyboard hints', async () => { | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={authQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| it('navigates between questions with arrow keys', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Which testing framework?', | |
| header: 'Testing', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Vitest', description: 'Fast unit testing' }], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Which CI provider?', | |
| header: 'CI', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'GitHub Actions', description: 'Built into GitHub' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toContain('Which testing framework?'); | |
| writeKey(stdin, '\x1b[C'); // Right arrow | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Which CI provider?'); | |
| }); | |
| writeKey(stdin, '\x1b[D'); // Left arrow | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Which testing framework?'); | |
| }); | |
| }); | |
| it('preserves answers when navigating back', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Which package manager?', | |
| header: 'Package', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'pnpm', description: 'Fast, disk efficient' }], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Which bundler?', | |
| header: 'Bundler', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Vite', description: 'Next generation bundler' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Answer first question (should auto-advance) | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Which bundler?'); | |
| }); | |
| // Navigate back | |
| writeKey(stdin, '\x1b[D'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Which package manager?'); | |
| }); | |
| // Navigate forward | |
| writeKey(stdin, '\x1b[C'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Which bundler?'); | |
| }); | |
| // Answer second question | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Review your answers:'); | |
| }); | |
| // Submit from Review | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({ '0': 'pnpm', '1': 'Vite' }); | |
| }); | |
| }); | |
| it('shows Review tab in progress header for multiple questions', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Which framework?', | |
| header: 'Framework', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'React', description: 'Component library' }, | |
| { label: 'Vue', description: 'Progressive framework' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Which styling?', | |
| header: 'Styling', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'Tailwind', description: 'Utility-first CSS' }, | |
| { label: 'CSS Modules', description: 'Scoped styles' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| it('allows navigating to Review tab and back', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Create tests?', | |
| header: 'Tests', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: 'Generate test files' }], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Add documentation?', | |
| header: 'Docs', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: 'Generate JSDoc comments' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| writeKey(stdin, '\x1b[C'); // Right arrow | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Add documentation?'); | |
| }); | |
| writeKey(stdin, '\x1b[C'); // Right arrow to Review | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| writeKey(stdin, '\x1b[D'); // Left arrow back | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Add documentation?'); | |
| }); | |
| }); | |
| it('shows warning for unanswered questions on Review tab', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Which license?', | |
| header: 'License', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'MIT', description: 'Permissive license' }], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Include README?', | |
| header: 'README', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: 'Generate README.md' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Navigate directly to Review tab without answering | |
| writeKey(stdin, '\x1b[C'); | |
| writeKey(stdin, '\x1b[C'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| }); | |
| it('submits with unanswered questions when user confirms on Review', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Target Node version?', | |
| header: 'Node', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Node 20', description: 'LTS version' }], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Enable strict mode?', | |
| header: 'Strict', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: 'Strict TypeScript' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Answer only first question | |
| writeKey(stdin, '\r'); | |
| // Navigate to Review tab | |
| writeKey(stdin, '\x1b[C'); | |
| // Submit | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({ '0': 'Node 20' }); | |
| }); | |
| }); | |
| describe('Text type questions', () => { | |
| it('renders text input for type: "text"', async () => { | |
| const textQuestion: Question[] = [ | |
| { | |
| question: 'What should we name this component?', | |
| header: 'Name', | |
| type: QuestionType.TEXT, | |
| placeholder: 'e.g., UserProfileCard', | |
| }, | |
| ]; | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={textQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| it('shows default placeholder when none provided', async () => { | |
| const textQuestion: Question[] = [ | |
| { | |
| question: 'Enter the database connection string:', | |
| header: 'Database', | |
| type: QuestionType.TEXT, | |
| }, | |
| ]; | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={textQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| it('supports backspace in text mode', async () => { | |
| const textQuestion: Question[] = [ | |
| { | |
| question: 'Enter the function name:', | |
| header: 'Function', | |
| type: QuestionType.TEXT, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={textQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| for (const char of 'abc') { | |
| writeKey(stdin, char); | |
| } | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('abc'); | |
| }); | |
| writeKey(stdin, '\x7f'); // Backspace | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('ab'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).not.toContain('abc'); | |
| }); | |
| }); | |
| it('shows correct keyboard hints for text type', async () => { | |
| const textQuestion: Question[] = [ | |
| { | |
| question: 'Enter the variable name:', | |
| header: 'Variable', | |
| type: QuestionType.TEXT, | |
| }, | |
| ]; | |
| const { lastFrame } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={textQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| it('preserves text answer when navigating between questions', async () => { | |
| const mixedQuestions: Question[] = [ | |
| { | |
| question: 'What should we name this hook?', | |
| header: 'Hook', | |
| type: QuestionType.TEXT, | |
| }, | |
| { | |
| question: 'Should it be async?', | |
| header: 'Async', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'Yes', description: 'Use async/await' }, | |
| { label: 'No', description: 'Synchronous hook' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={mixedQuestions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| for (const char of 'useAuth') { | |
| writeKey(stdin, char); | |
| } | |
| writeKey(stdin, '\t'); // Use Tab instead of Right arrow when text input is active | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Should it be async?'); | |
| }); | |
| writeKey(stdin, '\x1b[D'); // Left arrow should work when NOT focusing a text input | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('useAuth'); | |
| }); | |
| }); | |
| it('handles mixed text and choice questions', async () => { | |
| const mixedQuestions: Question[] = [ | |
| { | |
| question: 'What should we name this component?', | |
| header: 'Name', | |
| type: QuestionType.TEXT, | |
| placeholder: 'Enter component name', | |
| }, | |
| { | |
| question: 'Which styling approach?', | |
| header: 'Style', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'CSS Modules', description: 'Scoped CSS' }, | |
| { label: 'Tailwind', description: 'Utility classes' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={mixedQuestions} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| for (const char of 'DataTable') { | |
| writeKey(stdin, char); | |
| } | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Which styling approach?'); | |
| }); | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Review your answers:'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Name'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('DataTable'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Style'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('CSS Modules'); | |
| }); | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({ | |
| '0': 'DataTable', | |
| '1': 'CSS Modules', | |
| }); | |
| }); | |
| }); | |
| it('submits empty text as unanswered', async () => { | |
| const textQuestion: Question[] = [ | |
| { | |
| question: 'Enter the class name:', | |
| header: 'Class', | |
| type: QuestionType.TEXT, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={textQuestion} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({}); | |
| }); | |
| }); | |
| it('clears text on Ctrl+C', async () => { | |
| const textQuestion: Question[] = [ | |
| { | |
| question: 'Enter the class name:', | |
| header: 'Class', | |
| type: QuestionType.TEXT, | |
| }, | |
| ]; | |
| const onCancel = vi.fn(); | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={textQuestion} | |
| onSubmit={vi.fn()} | |
| onCancel={onCancel} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| for (const char of 'SomeText') { | |
| writeKey(stdin, char); | |
| } | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('SomeText'); | |
| }); | |
| // Send Ctrl+C | |
| writeKey(stdin, '\x03'); // Ctrl+C | |
| await waitFor(async () => { | |
| // Text should be cleared | |
| await waitUntilReady(); | |
| expect(lastFrame()).not.toContain('SomeText'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('>'); | |
| }); | |
| // Should NOT call onCancel (dialog should stay open) | |
| expect(onCancel).not.toHaveBeenCalled(); | |
| }); | |
| it('allows immediate arrow navigation after switching away from text input', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Choice Q?', | |
| header: 'Choice', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Option 1', description: '' }], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Text Q?', | |
| header: 'Text', | |
| type: QuestionType.TEXT, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // 1. Move to Text Q (Right arrow works for Choice Q) | |
| writeKey(stdin, '\x1b[C'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Text Q?'); | |
| }); | |
| // 2. Type something in Text Q to make isEditingCustomOption true | |
| writeKey(stdin, 'a'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('a'); | |
| }); | |
| // 3. Move back to Choice Q (Left arrow works because cursor is at left edge) | |
| // When typing 'a', cursor is at index 1. | |
| // We need to move cursor to index 0 first for Left arrow to work for navigation. | |
| writeKey(stdin, '\x1b[D'); // Left arrow moves cursor to index 0 | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Text Q?'); | |
| }); | |
| writeKey(stdin, '\x1b[D'); // Second Left arrow should now trigger navigation | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Choice Q?'); | |
| }); | |
| // 4. Immediately try Right arrow to go back to Text Q | |
| writeKey(stdin, '\x1b[C'); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Text Q?'); | |
| }); | |
| }); | |
| it('handles rapid sequential answers correctly (stale closure protection)', async () => { | |
| const multiQuestions: Question[] = [ | |
| { | |
| question: 'Question 1?', | |
| header: 'Q1', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'A1', description: '' }], | |
| multiSelect: false, | |
| }, | |
| { | |
| question: 'Question 2?', | |
| header: 'Q2', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'A2', description: '' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={multiQuestions} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Answer Q1 and Q2 sequentialy | |
| act(() => { | |
| stdin.write('\r'); // Select A1 for Q1 -> triggers autoAdvance | |
| }); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Question 2?'); | |
| }); | |
| act(() => { | |
| stdin.write('\r'); // Select A2 for Q2 -> triggers autoAdvance to Review | |
| }); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Review your answers:'); | |
| }); | |
| act(() => { | |
| stdin.write('\r'); // Submit from Review | |
| }); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({ | |
| '0': 'A1', | |
| '1': 'A2', | |
| }); | |
| }); | |
| }); | |
| }); | |
| describe('Markdown rendering', () => { | |
| it('auto-bolds plain single-line questions', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Which option do you prefer?', | |
| header: 'Test', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: '' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| availableHeight={40} | |
| />, | |
| { width: 120 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| // Plain text should be rendered as bold | |
| expect(frame).toContain('Which option do you prefer?'); | |
| }); | |
| }); | |
| it('does not auto-bold questions that already have markdown', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Is **this** working?', | |
| header: 'Test', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: '' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| availableHeight={40} | |
| />, | |
| { width: 120 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| // Should NOT have double-bold (the whole question bolded AND "this" bolded) | |
| // "Is " should not be bold, only "this" should be bold | |
| expect(frame).toContain('Is '); | |
| expect(frame).toContain('this'); | |
| expect(frame).not.toContain('**this**'); | |
| }); | |
| }); | |
| it('renders bold markdown in question', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Is **this** working?', | |
| header: 'Test', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: '' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| availableHeight={40} | |
| />, | |
| { width: 120 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| // Check for 'this' - asterisks should be gone | |
| expect(frame).toContain('this'); | |
| expect(frame).not.toContain('**this**'); | |
| }); | |
| }); | |
| it('renders inline code markdown in question', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Run `npm start`?', | |
| header: 'Test', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Yes', description: '' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={120} | |
| availableHeight={40} | |
| />, | |
| { width: 120 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| // Backticks should be removed | |
| expect(frame).toContain('Run npm start?'); | |
| expect(frame).not.toContain('`'); | |
| }); | |
| }); | |
| }); | |
| it('uses availableTerminalHeight from UIStateContext if availableHeight prop is missing', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Choose an option', | |
| header: 'Context Test', | |
| type: QuestionType.CHOICE, | |
| options: Array.from({ length: 10 }, (_, i) => ({ | |
| label: `Option ${i + 1}`, | |
| description: `Description ${i + 1}`, | |
| })), | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const mockUIState = { | |
| availableTerminalHeight: 5, // Small height to force scroll arrows | |
| } as UIState; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <UIStateContext.Provider value={mockUIState}> | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={80} | |
| /> | |
| </UIStateContext.Provider>, | |
| { | |
| config: makeFakeConfig({ useAlternateBuffer: false }), | |
| settings: createMockSettings({ ui: { useAlternateBuffer: false } }), | |
| }, | |
| ); | |
| // With height 5 and alternate buffer disabled, it should show scroll arrows (▲) | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('▲'); | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('▼'); | |
| }); | |
| it('does NOT truncate the question when in alternate buffer mode even with small height', async () => { | |
| const longQuestion = | |
| 'This is a very long question ' + 'with many words '.repeat(10); | |
| const questions: Question[] = [ | |
| { | |
| question: longQuestion, | |
| header: 'Alternate Buffer Test', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'Option 1', description: 'Desc 1' }], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const mockUIState = { | |
| availableTerminalHeight: 5, | |
| } as UIState; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <UIStateContext.Provider value={mockUIState}> | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={40} // Small width to force wrapping | |
| /> | |
| </UIStateContext.Provider>, | |
| { | |
| config: makeFakeConfig({ useAlternateBuffer: true }), | |
| settings: createMockSettings({ ui: { useAlternateBuffer: true } }), | |
| }, | |
| ); | |
| // Should NOT contain the truncation message | |
| await waitUntilReady(); | |
| expect(lastFrame()).not.toContain('hidden ...'); | |
| // Should contain the full long question (or at least its parts) | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('This is a very long question'); | |
| }); | |
| describe('Choice question placeholder', () => { | |
| it('uses placeholder for "Other" option when provided', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Select your preferred language:', | |
| header: 'Language', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'TypeScript', description: '' }, | |
| { label: 'JavaScript', description: '' }, | |
| ], | |
| placeholder: 'Type another language...', | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={80} | |
| />, | |
| { width: 80 }, | |
| ); | |
| // Navigate to the "Other" option | |
| writeKey(stdin, '\x1b[B'); // Down | |
| writeKey(stdin, '\x1b[B'); // Down to Other | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| }); | |
| it('uses default placeholder when not provided', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Select your preferred language:', | |
| header: 'Language', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'TypeScript', description: '' }, | |
| { label: 'JavaScript', description: '' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={80} | |
| />, | |
| { width: 80 }, | |
| ); | |
| // Navigate to the "Other" option | |
| writeKey(stdin, '\x1b[B'); // Down | |
| writeKey(stdin, '\x1b[B'); // Down to Other | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| }); | |
| it('supports "Other" option for yesno questions', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Is this correct?', | |
| header: 'Confirm', | |
| type: QuestionType.YESNO, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={80} | |
| />, | |
| { width: 80 }, | |
| ); | |
| // Navigate to "Other" (3rd option: 1. Yes, 2. No, 3. Other) | |
| writeKey(stdin, '\x1b[B'); // Down to No | |
| writeKey(stdin, '\x1b[B'); // Down to Other | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Enter a custom value'); | |
| }); | |
| // Type feedback | |
| for (const char of 'Yes, but with caveats') { | |
| writeKey(stdin, char); | |
| } | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toContain('Yes, but with caveats'); | |
| }); | |
| // Submit | |
| writeKey(stdin, '\r'); | |
| await waitFor(async () => { | |
| expect(onSubmit).toHaveBeenCalledWith({ '0': 'Yes, but with caveats' }); | |
| }); | |
| }); | |
| }); | |
| it('expands paste placeholders in multi-select custom option via Done', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Which features?', | |
| header: 'Features', | |
| type: QuestionType.CHOICE, | |
| options: [{ label: 'TypeScript', description: '' }], | |
| multiSelect: true, | |
| }, | |
| ]; | |
| const onSubmit = vi.fn(); | |
| const { stdin } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={onSubmit} | |
| onCancel={vi.fn()} | |
| width={120} | |
| />, | |
| { width: 120 }, | |
| ); | |
| // Select TypeScript | |
| writeKey(stdin, '\r'); | |
| // Down to Other | |
| writeKey(stdin, '\x1b[B'); | |
| // Simulate bracketed paste of multi-line text into the custom option | |
| const pastedText = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6'; | |
| const ESC = '\x1b'; | |
| writeKey(stdin, `${ESC}[200~${pastedText}${ESC}[201~`); | |
| // Down to Done and submit | |
| writeKey(stdin, '\x1b[B'); | |
| writeKey(stdin, '\r'); | |
| await waitFor(() => { | |
| expect(onSubmit).toHaveBeenCalledWith({ | |
| '0': `TypeScript, ${pastedText}`, | |
| }); | |
| }); | |
| }); | |
| it('shows at least 3 selection options even in small terminal heights', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: | |
| 'A very long question that would normally take up most of the space and squeeze the list if we did not have a heuristic to prevent it. This line is just to make it longer. And another one. Imagine this is a plan.', | |
| header: 'Test', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'Option 1', description: 'Description 1' }, | |
| { label: 'Option 2', description: 'Description 2' }, | |
| { label: 'Option 3', description: 'Description 3' }, | |
| { label: 'Option 4', description: 'Description 4' }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={80} | |
| availableHeight={12} // Very small height | |
| />, | |
| { width: 80 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| // Should show at least 3 options | |
| expect(frame).toContain('1. Option 1'); | |
| expect(frame).toContain('2. Option 2'); | |
| expect(frame).toContain('3. Option 3'); | |
| }); | |
| }); | |
| it('allows the question to exceed 15 lines in a tall terminal', async () => { | |
| const longQuestion = Array.from( | |
| { length: 25 }, | |
| (_, i) => `Line ${i + 1}`, | |
| ).join('\n'); | |
| const questions: Question[] = [ | |
| { | |
| question: longQuestion, | |
| header: 'Tall Test', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { label: 'Option 1', description: 'D1' }, | |
| { label: 'Option 2', description: 'D2' }, | |
| { label: 'Option 3', description: 'D3' }, | |
| ], | |
| multiSelect: false, | |
| unconstrainedHeight: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={80} | |
| availableHeight={40} // Tall terminal | |
| />, | |
| { width: 80 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| // Should show more than 15 lines of the question | |
| // (The limit was previously 15, so showing Line 20 proves it's working) | |
| expect(frame).toContain('Line 20'); | |
| expect(frame).toContain('Line 25'); | |
| // Should still show the options | |
| expect(frame).toContain('1. Option 1'); | |
| }); | |
| }); | |
| it('indents multi-line descriptions correctly', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Single choice?', | |
| header: 'Indent Test', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { | |
| label: 'Option 1', | |
| description: | |
| 'This is a very long description that is expected to wrap onto multiple lines in a narrow terminal. We want to ensure that all lines are correctly indented.', | |
| }, | |
| ], | |
| multiSelect: false, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={40} // Narrow width to force wrapping | |
| />, | |
| { width: 40 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| // Snapshot will capture the visual alignment | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| }); | |
| it('indents multi-line descriptions correctly in multi-select mode', async () => { | |
| const questions: Question[] = [ | |
| { | |
| question: 'Multi-select?', | |
| header: 'Indent Test', | |
| type: QuestionType.CHOICE, | |
| options: [ | |
| { | |
| label: 'Option 1', | |
| description: | |
| 'This is a very long description that is expected to wrap onto multiple lines in a narrow terminal. We want to ensure that all lines are correctly indented even with checkboxes.', | |
| }, | |
| ], | |
| multiSelect: true, | |
| }, | |
| ]; | |
| const { lastFrame, waitUntilReady } = await renderWithProviders( | |
| <AskUserDialog | |
| questions={questions} | |
| onSubmit={vi.fn()} | |
| onCancel={vi.fn()} | |
| width={40} // Narrow width to force wrapping | |
| />, | |
| { width: 40 }, | |
| ); | |
| await waitFor(async () => { | |
| await waitUntilReady(); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| }); | |
| }); | |
| }); | |