2026-01-23 15:42:48 -05:00
/ * *
* @license
* Copyright 2026 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2026-02-11 09:14:53 -05:00
import { describe , it , expect , vi , afterEach , beforeEach } from 'vitest' ;
2026-01-23 15:42:48 -05:00
import { act } from 'react' ;
import { renderWithProviders } from '../../test-utils/render.js' ;
2026-03-18 16:38:56 +00:00
import { createMockSettings } from '../../test-utils/settings.js' ;
import { makeFakeConfig } from '@google/gemini-cli-core' ;
2026-01-23 15:42:48 -05:00
import { waitFor } from '../../test-utils/async.js' ;
import { AskUserDialog } from './AskUserDialog.js' ;
import { QuestionType , type Question } from '@google/gemini-cli-core' ;
2026-01-30 17:07:41 -08:00
import { UIStateContext , type UIState } from '../contexts/UIStateContext.js' ;
2026-01-23 15:42:48 -05:00
// Helper to write to stdin with proper act() wrapping
const writeKey = ( stdin : { write : ( data : string ) = > void } , key : string ) = > {
act ( ( ) = > {
stdin . write ( key ) ;
} ) ;
} ;
describe ( 'AskUserDialog' , ( ) = > {
2026-02-11 09:14:53 -05:00
// 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 ) ) ;
} ) ;
2026-01-23 15:42:48 -05:00
afterEach ( ( ) = > {
vi . restoreAllMocks ( ) ;
} ) ;
const authQuestion : Question [ ] = [
{
question : 'Which authentication method should we use?' ,
header : 'Auth' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'OAuth 2.0' , description : 'Industry standard, supports SSO' } ,
{ label : 'JWT tokens' , description : 'Stateless, good for APIs' } ,
] ,
multiSelect : false ,
} ,
] ;
2026-02-18 16:46:50 -08:00
it ( 'renders question and options' , async ( ) = > {
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { authQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
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' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
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
2026-03-17 15:17:34 -04:00
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
2026-01-23 15:42:48 -05:00
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 ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { questions }
onSubmit = { onSubmit }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
actions ( stdin ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( expectedSubmit ) ;
} ) ;
} ) ;
} ) ;
2026-03-17 15:17:34 -04:00
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 [ ] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-03-17 15:17:34 -04:00
< 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 ( ) ;
} ) ;
} ) ;
2026-01-23 15:42:48 -05:00
it ( 'handles custom option in single select with inline typing' , async ( ) = > {
const onSubmit = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { authQuestion }
onSubmit = { onSubmit }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
// Move down to custom option
writeKey ( stdin , '\x1b[B' ) ;
writeKey ( stdin , '\x1b[B' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Enter a custom value' ) ;
} ) ;
// Type directly (inline)
for ( const char of 'API Key' ) {
writeKey ( stdin , char ) ;
}
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'API Key' ) ;
} ) ;
// Press Enter to submit the custom value
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( { '0' : 'API Key' } ) ;
} ) ;
} ) ;
2026-02-11 09:14:53 -05:00
it ( 'supports multi-line input for "Other" option in choice questions' , async ( ) = > {
const authQuestionWithOther : Question [ ] = [
{
question : 'Which authentication method?' ,
header : 'Auth' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-02-11 09:14:53 -05:00
options : [ { label : 'OAuth 2.0' , description : '' } ] ,
multiSelect : false ,
} ,
] ;
const onSubmit = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-02-11 09:14:53 -05:00
< 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 ) ;
}
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-02-11 09:14:53 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Line 1' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-02-11 09:14:53 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Line 2' ) ;
} ) ;
// Press Enter to submit
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-02-11 09:14:53 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( { '0' : 'Line 1\nLine 2' } ) ;
} ) ;
} ) ;
2026-01-30 17:07:41 -08:00
describe . each ( [
{ useAlternateBuffer : true , expectedArrows : false } ,
{ 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' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-30 17:07:41 -08:00
options : Array.from ( { length : 15 } , ( _ , i ) = > ( {
label : ` Option ${ i + 1 } ` ,
description : ` Description ${ i + 1 } ` ,
} ) ) ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-30 17:07:41 -08:00
< AskUserDialog
questions = { questions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
width = { 80 }
availableHeight = { 10 } // Small height to force scrolling
/ > ,
2026-03-18 16:38:56 +00:00
{
config : makeFakeConfig ( { useAlternateBuffer } ) ,
2026-03-18 18:12:44 +00:00
settings : createMockSettings ( { ui : { useAlternateBuffer } } ) ,
2026-03-18 16:38:56 +00:00
} ,
2026-01-30 17:07:41 -08:00
) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-30 17:07:41 -08:00
if ( expectedArrows ) {
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . toContain ( '▲' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . toContain ( '▼' ) ;
} else {
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . not . toContain ( '▲' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . not . toContain ( '▼' ) ;
}
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
} ) ;
} ,
) ;
2026-01-30 13:32:21 -05:00
2026-01-23 15:42:48 -05:00
it ( 'navigates to custom option when typing unbound characters (Type-to-Jump)' , async ( ) = > {
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { authQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
// Type a character without navigating down
writeKey ( stdin , 'A' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
// Should show the custom input with 'A'
// Placeholder is hidden when text is present
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'A' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( '3. A' ) ;
} ) ;
// Continue typing
writeKey ( stdin , 'P' ) ;
writeKey ( stdin , 'I' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'API' ) ;
} ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'shows progress header for multiple questions' , async ( ) = > {
2026-01-23 15:42:48 -05:00
const multiQuestions : Question [ ] = [
{
question : 'Which database should we use?' ,
header : 'Database' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'PostgreSQL' , description : 'Relational database' } ,
{ label : 'MongoDB' , description : 'Document database' } ,
] ,
multiSelect : false ,
} ,
{
question : 'Which ORM do you prefer?' ,
header : 'ORM' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'Prisma' , description : 'Type-safe ORM' } ,
{ label : 'Drizzle' , description : 'Lightweight ORM' } ,
] ,
multiSelect : false ,
} ,
] ;
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'hides progress header for single question' , async ( ) = > {
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { authQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'shows keyboard hints' , async ( ) = > {
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { authQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
it ( 'navigates between questions with arrow keys' , async ( ) = > {
const multiQuestions : Question [ ] = [
{
question : 'Which testing framework?' ,
header : 'Testing' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Vitest' , description : 'Fast unit testing' } ] ,
multiSelect : false ,
} ,
{
question : 'Which CI provider?' ,
header : 'CI' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'GitHub Actions' , description : 'Built into GitHub' } ,
] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
expect ( lastFrame ( ) ) . toContain ( 'Which testing framework?' ) ;
writeKey ( stdin , '\x1b[C' ) ; // Right arrow
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Which CI provider?' ) ;
} ) ;
writeKey ( stdin , '\x1b[D' ) ; // Left arrow
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Which testing framework?' ) ;
} ) ;
} ) ;
it ( 'preserves answers when navigating back' , async ( ) = > {
const multiQuestions : Question [ ] = [
{
question : 'Which package manager?' ,
header : 'Package' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'pnpm' , description : 'Fast, disk efficient' } ] ,
multiSelect : false ,
} ,
{
question : 'Which bundler?' ,
header : 'Bundler' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Vite' , description : 'Next generation bundler' } ] ,
multiSelect : false ,
} ,
] ;
const onSubmit = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { onSubmit }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
// Answer first question (should auto-advance)
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Which bundler?' ) ;
} ) ;
// Navigate back
writeKey ( stdin , '\x1b[D' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Which package manager?' ) ;
} ) ;
// Navigate forward
writeKey ( stdin , '\x1b[C' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Which bundler?' ) ;
} ) ;
// Answer second question
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Review your answers:' ) ;
} ) ;
// Submit from Review
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( { '0' : 'pnpm' , '1' : 'Vite' } ) ;
} ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'shows Review tab in progress header for multiple questions' , async ( ) = > {
2026-01-23 15:42:48 -05:00
const multiQuestions : Question [ ] = [
{
question : 'Which framework?' ,
header : 'Framework' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'React' , description : 'Component library' } ,
{ label : 'Vue' , description : 'Progressive framework' } ,
] ,
multiSelect : false ,
} ,
{
question : 'Which styling?' ,
header : 'Styling' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'Tailwind' , description : 'Utility-first CSS' } ,
{ label : 'CSS Modules' , description : 'Scoped styles' } ,
] ,
multiSelect : false ,
} ,
] ;
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
it ( 'allows navigating to Review tab and back' , async ( ) = > {
const multiQuestions : Question [ ] = [
{
question : 'Create tests?' ,
header : 'Tests' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Yes' , description : 'Generate test files' } ] ,
multiSelect : false ,
} ,
{
question : 'Add documentation?' ,
header : 'Docs' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Yes' , description : 'Generate JSDoc comments' } ] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
writeKey ( stdin , '\x1b[C' ) ; // Right arrow
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Add documentation?' ) ;
} ) ;
writeKey ( stdin , '\x1b[C' ) ; // Right arrow to Review
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
writeKey ( stdin , '\x1b[D' ) ; // Left arrow back
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Add documentation?' ) ;
} ) ;
} ) ;
it ( 'shows warning for unanswered questions on Review tab' , async ( ) = > {
const multiQuestions : Question [ ] = [
{
question : 'Which license?' ,
header : 'License' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'MIT' , description : 'Permissive license' } ] ,
multiSelect : false ,
} ,
{
question : 'Include README?' ,
header : 'README' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Yes' , description : 'Generate README.md' } ] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
// Navigate directly to Review tab without answering
writeKey ( stdin , '\x1b[C' ) ;
writeKey ( stdin , '\x1b[C' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
} ) ;
it ( 'submits with unanswered questions when user confirms on Review' , async ( ) = > {
const multiQuestions : Question [ ] = [
{
question : 'Target Node version?' ,
header : 'Node' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Node 20' , description : 'LTS version' } ] ,
multiSelect : false ,
} ,
{
question : 'Enable strict mode?' ,
header : 'Strict' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Yes' , description : 'Strict TypeScript' } ] ,
multiSelect : false ,
} ,
] ;
const onSubmit = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { onSubmit }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
// Answer only first question
writeKey ( stdin , '\r' ) ;
// Navigate to Review tab
writeKey ( stdin , '\x1b[C' ) ;
// Submit
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( { '0' : 'Node 20' } ) ;
} ) ;
} ) ;
describe ( 'Text type questions' , ( ) = > {
2026-02-18 16:46:50 -08:00
it ( 'renders text input for type: "text"' , async ( ) = > {
2026-01-23 15:42:48 -05:00
const textQuestion : Question [ ] = [
{
question : 'What should we name this component?' ,
header : 'Name' ,
type : QuestionType . TEXT ,
placeholder : 'e.g., UserProfileCard' ,
} ,
] ;
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { textQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'shows default placeholder when none provided' , async ( ) = > {
2026-01-23 15:42:48 -05:00
const textQuestion : Question [ ] = [
{
question : 'Enter the database connection string:' ,
header : 'Database' ,
type : QuestionType . TEXT ,
} ,
] ;
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { textQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
it ( 'supports backspace in text mode' , async ( ) = > {
const textQuestion : Question [ ] = [
{
question : 'Enter the function name:' ,
header : 'Function' ,
type : QuestionType . TEXT ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { textQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
for ( const char of 'abc' ) {
writeKey ( stdin , char ) ;
}
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'abc' ) ;
} ) ;
writeKey ( stdin , '\x7f' ) ; // Backspace
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'ab' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . not . toContain ( 'abc' ) ;
} ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'shows correct keyboard hints for text type' , async ( ) = > {
2026-01-23 15:42:48 -05:00
const textQuestion : Question [ ] = [
{
question : 'Enter the variable name:' ,
header : 'Variable' ,
type : QuestionType . TEXT ,
} ,
] ;
2026-03-20 20:08:29 +00:00
const { lastFrame } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { textQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
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' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'Yes' , description : 'Use async/await' } ,
{ label : 'No' , description : 'Synchronous hook' } ,
] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { mixedQuestions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
for ( const char of 'useAuth' ) {
writeKey ( stdin , char ) ;
}
writeKey ( stdin , '\t' ) ; // Use Tab instead of Right arrow when text input is active
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Should it be async?' ) ;
} ) ;
writeKey ( stdin , '\x1b[D' ) ; // Left arrow should work when NOT focusing a text input
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
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' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [
{ label : 'CSS Modules' , description : 'Scoped CSS' } ,
{ label : 'Tailwind' , description : 'Utility classes' } ,
] ,
multiSelect : false ,
} ,
] ;
const onSubmit = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { mixedQuestions }
onSubmit = { onSubmit }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
for ( const char of 'DataTable' ) {
writeKey ( stdin , char ) ;
}
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Which styling approach?' ) ;
} ) ;
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Review your answers:' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Name' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'DataTable' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Style' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'CSS Modules' ) ;
} ) ;
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( {
'0' : 'DataTable' ,
'1' : 'CSS Modules' ,
} ) ;
} ) ;
} ) ;
2026-02-11 09:14:53 -05:00
it ( 'submits empty text as unanswered' , async ( ) = > {
2026-01-23 15:42:48 -05:00
const textQuestion : Question [ ] = [
{
question : 'Enter the class name:' ,
header : 'Class' ,
type : QuestionType . TEXT ,
} ,
] ;
const onSubmit = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { textQuestion }
onSubmit = { onSubmit }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
writeKey ( stdin , '\r' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-02-11 09:14:53 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( { } ) ;
} ) ;
2026-01-23 15:42:48 -05:00
} ) ;
it ( 'clears text on Ctrl+C' , async ( ) = > {
const textQuestion : Question [ ] = [
{
question : 'Enter the class name:' ,
header : 'Class' ,
type : QuestionType . TEXT ,
} ,
] ;
const onCancel = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { textQuestion }
onSubmit = { vi . fn ( ) }
onCancel = { onCancel }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
for ( const char of 'SomeText' ) {
writeKey ( stdin , char ) ;
}
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'SomeText' ) ;
} ) ;
// Send Ctrl+C
writeKey ( stdin , '\x03' ) ; // Ctrl+C
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
// Text should be cleared
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . not . toContain ( 'SomeText' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
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' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'Option 1' , description : '' } ] ,
multiSelect : false ,
} ,
{
question : 'Text Q?' ,
header : 'Text' ,
type : QuestionType . TEXT ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
// 1. Move to Text Q (Right arrow works for Choice Q)
writeKey ( stdin , '\x1b[C' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Text Q?' ) ;
} ) ;
// 2. Type something in Text Q to make isEditingCustomOption true
writeKey ( stdin , 'a' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
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
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Text Q?' ) ;
} ) ;
writeKey ( stdin , '\x1b[D' ) ; // Second Left arrow should now trigger navigation
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Choice Q?' ) ;
} ) ;
// 4. Immediately try Right arrow to go back to Text Q
writeKey ( stdin , '\x1b[C' ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Text Q?' ) ;
} ) ;
} ) ;
it ( 'handles rapid sequential answers correctly (stale closure protection)' , async ( ) = > {
const multiQuestions : Question [ ] = [
{
question : 'Question 1?' ,
header : 'Q1' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'A1' , description : '' } ] ,
multiSelect : false ,
} ,
{
question : 'Question 2?' ,
header : 'Q2' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-23 15:42:48 -05:00
options : [ { label : 'A2' , description : '' } ] ,
multiSelect : false ,
} ,
] ;
const onSubmit = vi . fn ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-23 15:42:48 -05:00
< AskUserDialog
questions = { multiQuestions }
onSubmit = { onSubmit }
onCancel = { vi . fn ( ) }
2026-01-30 13:32:21 -05:00
width = { 120 }
2026-01-23 15:42:48 -05:00
/ > ,
2026-01-27 14:26:00 -08:00
{ width : 120 } ,
2026-01-23 15:42:48 -05:00
) ;
// Answer Q1 and Q2 sequentialy
act ( ( ) = > {
stdin . write ( '\r' ) ; // Select A1 for Q1 -> triggers autoAdvance
} ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Question 2?' ) ;
} ) ;
act ( ( ) = > {
stdin . write ( '\r' ) ; // Select A2 for Q2 -> triggers autoAdvance to Review
} ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-01-23 15:42:48 -05:00
expect ( lastFrame ( ) ) . toContain ( 'Review your answers:' ) ;
} ) ;
act ( ( ) = > {
stdin . write ( '\r' ) ; // Submit from Review
} ) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
2026-01-23 15:42:48 -05:00
expect ( onSubmit ) . toHaveBeenCalledWith ( {
'0' : 'A1' ,
'1' : 'A2' ,
} ) ;
} ) ;
} ) ;
} ) ;
2026-01-30 17:07:41 -08:00
2026-02-03 16:04:38 -05:00
describe ( 'Markdown rendering' , ( ) = > {
it ( 'auto-bolds plain single-line questions' , async ( ) = > {
const questions : Question [ ] = [
{
question : 'Which option do you prefer?' ,
header : 'Test' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-02-03 16:04:38 -05:00
options : [ { label : 'Yes' , description : '' } ] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { lastFrame , waitUntilReady } = await renderWithProviders (
2026-02-03 16:04:38 -05:00
< AskUserDialog
questions = { questions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
width = { 120 }
availableHeight = { 40 }
/ > ,
{ width : 120 } ,
) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-02-03 16:04:38 -05:00
const frame = lastFrame ( ) ;
// Plain text should be rendered as bold
2026-02-25 15:31:35 -08:00
expect ( frame ) . toContain ( 'Which option do you prefer?' ) ;
2026-02-03 16:04:38 -05:00
} ) ;
} ) ;
it ( 'does not auto-bold questions that already have markdown' , async ( ) = > {
const questions : Question [ ] = [
{
question : 'Is **this** working?' ,
header : 'Test' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-02-03 16:04:38 -05:00
options : [ { label : 'Yes' , description : '' } ] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { lastFrame , waitUntilReady } = await renderWithProviders (
2026-02-03 16:04:38 -05:00
< AskUserDialog
questions = { questions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
width = { 120 }
availableHeight = { 40 }
/ > ,
{ width : 120 } ,
) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-02-03 16:04:38 -05:00
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 ' ) ;
2026-02-25 15:31:35 -08:00
expect ( frame ) . toContain ( 'this' ) ;
2026-02-03 16:04:38 -05:00
expect ( frame ) . not . toContain ( '**this**' ) ;
} ) ;
} ) ;
it ( 'renders bold markdown in question' , async ( ) = > {
const questions : Question [ ] = [
{
question : 'Is **this** working?' ,
header : 'Test' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-02-03 16:04:38 -05:00
options : [ { label : 'Yes' , description : '' } ] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { lastFrame , waitUntilReady } = await renderWithProviders (
2026-02-03 16:04:38 -05:00
< AskUserDialog
questions = { questions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
width = { 120 }
availableHeight = { 40 }
/ > ,
{ width : 120 } ,
) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-02-03 16:04:38 -05:00
const frame = lastFrame ( ) ;
2026-02-25 15:31:35 -08:00
// Check for 'this' - asterisks should be gone
expect ( frame ) . toContain ( 'this' ) ;
2026-02-03 16:04:38 -05:00
expect ( frame ) . not . toContain ( '**this**' ) ;
} ) ;
} ) ;
it ( 'renders inline code markdown in question' , async ( ) = > {
const questions : Question [ ] = [
{
question : 'Run `npm start`?' ,
header : 'Test' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-02-03 16:04:38 -05:00
options : [ { label : 'Yes' , description : '' } ] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { lastFrame , waitUntilReady } = await renderWithProviders (
2026-02-03 16:04:38 -05:00
< AskUserDialog
questions = { questions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
width = { 120 }
availableHeight = { 40 }
/ > ,
{ width : 120 } ,
) ;
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-02-03 16:04:38 -05:00
const frame = lastFrame ( ) ;
// Backticks should be removed
2026-02-25 15:31:35 -08:00
expect ( frame ) . toContain ( 'Run npm start?' ) ;
expect ( frame ) . not . toContain ( '`' ) ;
2026-02-03 16:04:38 -05:00
} ) ;
} ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'uses availableTerminalHeight from UIStateContext if availableHeight prop is missing' , async ( ) = > {
2026-01-30 17:07:41 -08:00
const questions : Question [ ] = [
{
question : 'Choose an option' ,
header : 'Context Test' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-30 17:07:41 -08:00
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 ;
2026-03-19 17:05:33 +00:00
const { lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-30 17:07:41 -08:00
< UIStateContext.Provider value = { mockUIState } >
< AskUserDialog
questions = { questions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
width = { 80 }
/ >
< / UIStateContext.Provider > ,
2026-03-18 16:38:56 +00:00
{
config : makeFakeConfig ( { useAlternateBuffer : false } ) ,
2026-03-18 18:12:44 +00:00
settings : createMockSettings ( { ui : { useAlternateBuffer : false } } ) ,
2026-03-18 16:38:56 +00:00
} ,
2026-01-30 17:07:41 -08:00
) ;
// With height 5 and alternate buffer disabled, it should show scroll arrows (▲)
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . toContain ( '▲' ) ;
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . toContain ( '▼' ) ;
} ) ;
2026-02-18 16:46:50 -08:00
it ( 'does NOT truncate the question when in alternate buffer mode even with small height' , async ( ) = > {
2026-01-30 17:07:41 -08:00
const longQuestion =
'This is a very long question ' + 'with many words ' . repeat ( 10 ) ;
const questions : Question [ ] = [
{
question : longQuestion ,
header : 'Alternate Buffer Test' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-01-30 17:07:41 -08:00
options : [ { label : 'Option 1' , description : 'Desc 1' } ] ,
multiSelect : false ,
} ,
] ;
const mockUIState = {
availableTerminalHeight : 5 ,
} as UIState ;
2026-03-19 17:05:33 +00:00
const { lastFrame , waitUntilReady } = await renderWithProviders (
2026-01-30 17:07:41 -08:00
< UIStateContext.Provider value = { mockUIState } >
< AskUserDialog
questions = { questions }
onSubmit = { vi . fn ( ) }
onCancel = { vi . fn ( ) }
width = { 40 } // Small width to force wrapping
/ >
< / UIStateContext.Provider > ,
2026-03-18 16:38:56 +00:00
{
config : makeFakeConfig ( { useAlternateBuffer : true } ) ,
2026-03-18 18:12:44 +00:00
settings : createMockSettings ( { ui : { useAlternateBuffer : true } } ) ,
2026-03-18 16:38:56 +00:00
} ,
2026-01-30 17:07:41 -08:00
) ;
// Should NOT contain the truncation message
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . not . toContain ( 'hidden ...' ) ;
// Should contain the full long question (or at least its parts)
2026-02-18 16:46:50 -08:00
await waitUntilReady ( ) ;
2026-01-30 17:07:41 -08:00
expect ( lastFrame ( ) ) . toContain ( 'This is a very long question' ) ;
} ) ;
2026-02-02 12:00:13 -05:00
describe ( 'Choice question placeholder' , ( ) = > {
it ( 'uses placeholder for "Other" option when provided' , async ( ) = > {
const questions : Question [ ] = [
{
question : 'Select your preferred language:' ,
header : 'Language' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-02-02 12:00:13 -05:00
options : [
{ label : 'TypeScript' , description : '' } ,
{ label : 'JavaScript' , description : '' } ,
] ,
placeholder : 'Type another language...' ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-02-02 12:00:13 -05:00
< 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
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-02-02 12:00:13 -05:00
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
} ) ;
it ( 'uses default placeholder when not provided' , async ( ) = > {
const questions : Question [ ] = [
{
question : 'Select your preferred language:' ,
header : 'Language' ,
2026-02-13 10:03:52 -05:00
type : QuestionType . CHOICE ,
2026-02-02 12:00:13 -05:00
options : [
{ label : 'TypeScript' , description : '' } ,
{ label : 'JavaScript' , description : '' } ,
] ,
multiSelect : false ,
} ,
] ;
2026-03-19 17:05:33 +00:00
const { stdin , lastFrame , waitUntilReady } = await renderWithProviders (
2026-02-02 12:00:13 -05:00
< 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
2026-02-18 16:46:50 -08:00
await waitFor ( async ( ) = > {
await waitUntilReady ( ) ;
2026-02-02 12:00:13 -05:00
expect ( lastFrame ( ) ) . toMatchSnapshot ( ) ;
} ) ;
} ) ;
} ) ;
2026-03-06 22:29:38 -05:00
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 ( ) ;
2026-03-19 17:05:33 +00:00
const { stdin } = await renderWithProviders (
2026-03-06 22:29:38 -05:00
< 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 } ` ,
} ) ;
} ) ;
} ) ;
2026-03-23 14:27:08 -04:00
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' ) ;
} ) ;
} ) ;
2026-01-23 15:42:48 -05:00
} ) ;