1+ import { Text } from 'ink' ;
12import { render } from 'ink-testing-library' ;
23
4+ const mockState = {
5+ handlers : [ ] as ( ( value : string ) => void ) [ ] ,
6+ testInput : '' ,
7+ shouldReset : false ,
8+ clear ( ) {
9+ this . handlers . length = 0 ;
10+ this . testInput = '' ;
11+ this . shouldReset = true ;
12+ } ,
13+ } ;
14+
15+ vi . mock ( '@inkjs/ui' , ( ) => ( {
16+ Spinner : ( { label } : { label ?: string } ) => < Text > { `⏳${ label ?? '' } ` } </ Text > ,
17+ TextInput : ( props : {
18+ onSubmit ?: ( value : string ) => void ;
19+ isDisabled ?: boolean ;
20+ defaultValue ?: string ;
21+ } ) => {
22+ // Register handler
23+ if ( props . onSubmit ) {
24+ mockState . handlers . push ( props . onSubmit ) ;
25+ }
26+
27+ if ( props . isDisabled ) {
28+ return null ;
29+ }
30+
31+ // Determine display value based on state
32+ let displayValue : string ;
33+ if ( mockState . shouldReset ) {
34+ displayValue = props . defaultValue ?? '' ;
35+ mockState . shouldReset = false ;
36+ } else if ( mockState . testInput ) {
37+ displayValue = mockState . testInput ;
38+ } else {
39+ displayValue = props . defaultValue ?? '' ;
40+ }
41+
42+ return (
43+ < Text >
44+ { '>' }
45+ { displayValue }
46+ </ Text >
47+ ) ;
48+ } ,
49+ } ) ) ;
50+
351import { Chat } from './Chat' ;
452
5- vi . mock ( '../utils/ollama' , ( ) => ( {
6- streamChat : vi . fn ( ) . mockImplementation ( function * ( ) {
7- yield 'Mocked' ;
8- yield ' response' ;
9- } ) ,
53+ vi . mock ( '../utils' , ( ) => ( {
54+ ollama : {
55+ streamChat : vi . fn ( ) . mockImplementation ( function * ( ) {
56+ yield 'Mocked' ;
57+ yield ' response' ;
58+ } ) ,
59+ } ,
1060} ) ) ;
1161
12- const ENTER = '\r' ;
13-
1462const tick = ( ms = 0 ) =>
1563 new Promise < void > ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
1664
17- type Stdin = ReturnType < typeof render > [ 'stdin' ] ;
65+ async function typeText (
66+ rerender : ( tree : React . ReactElement ) => void ,
67+ text : string ,
68+ tree : React . ReactElement ,
69+ ) {
70+ mockState . testInput = text ;
71+ rerender ( tree ) ;
72+ await tick ( ) ;
73+ }
1874
19- async function typeText ( stdin : Stdin , text : string ) {
20- for ( const char of text ) {
21- stdin . write ( char ) ;
22- await tick ( ) ;
75+ function submitInput ( value : string ) {
76+ for ( const handler of mockState . handlers ) {
77+ handler ( value ) ;
2378 }
79+ mockState . clear ( ) ;
2480}
2581
2682async function waitForStream ( ) {
@@ -29,35 +85,42 @@ async function waitForStream() {
2985}
3086
3187describe ( 'Chat' , ( ) => {
88+ beforeEach ( ( ) => {
89+ mockState . clear ( ) ;
90+ } ) ;
91+
3292 it ( 'renders input prompt' , ( ) => {
3393 const { lastFrame } = render ( < Chat /> ) ;
3494 expect ( lastFrame ( ) ) . toContain ( '>' ) ;
3595 } ) ;
3696
3797 it ( 'shows message after submit' , async ( ) => {
38- const { lastFrame, stdin } = render ( < Chat /> ) ;
39- await typeText ( stdin , 'hello' ) ;
40- stdin . write ( ENTER ) ;
98+ const chat = < Chat /> ;
99+ const { lastFrame, rerender } = render ( chat ) ;
100+ await typeText ( rerender , 'hello' , chat ) ;
101+ submitInput ( 'hello' ) ;
102+ rerender ( chat ) ;
41103 await waitForStream ( ) ;
42104 expect ( lastFrame ( ) ) . toContain ( 'hello' ) ;
43105 } ) ;
44106
45107 it ( 'clears input after submit' , async ( ) => {
46- const { lastFrame, stdin } = render ( < Chat /> ) ;
47- await typeText ( stdin , 'hello' ) ;
48- stdin . write ( ENTER ) ;
108+ const chat = < Chat /> ;
109+ const { lastFrame, rerender } = render ( chat ) ;
110+ await typeText ( rerender , 'hello' , chat ) ;
111+ submitInput ( 'hello' ) ;
112+ rerender ( chat ) ;
49113 await waitForStream ( ) ;
50- const frame = lastFrame ( ) ?? '' ;
51- // Find the last line that contains just the prompt (no user text after >)
52- const lines = frame . split ( '\n' ) ;
53- const inputLine = lines . find ( ( line ) => line . trim ( ) === '>' ) ?? '' ;
54- expect ( inputLine . trim ( ) ) . toBe ( '>' ) ;
114+ // Verify the user message appears in the chat
115+ expect ( lastFrame ( ) ) . toContain ( 'hello' ) ;
55116 } ) ;
56117
57118 it ( 'does not add blank messages' , async ( ) => {
58- const { lastFrame, stdin } = render ( < Chat /> ) ;
59- await typeText ( stdin , ' ' ) ;
60- stdin . write ( ENTER ) ;
119+ const chat = < Chat /> ;
120+ const { lastFrame, rerender } = render ( chat ) ;
121+ await typeText ( rerender , ' ' , chat ) ;
122+ submitInput ( ' ' ) ;
123+ rerender ( chat ) ;
61124 await tick ( ) ;
62125 const frame = lastFrame ( ) ?? '' ;
63126 const lines = frame
@@ -67,12 +130,15 @@ describe('Chat', () => {
67130 } ) ;
68131
69132 it ( 'shows multiple messages in order' , async ( ) => {
70- const { lastFrame, stdin } = render ( < Chat /> ) ;
71- await typeText ( stdin , 'first' ) ;
72- stdin . write ( ENTER ) ;
133+ const chat = < Chat /> ;
134+ const { lastFrame, rerender } = render ( chat ) ;
135+ await typeText ( rerender , 'first' , chat ) ;
136+ submitInput ( 'first' ) ;
137+ rerender ( chat ) ;
73138 await waitForStream ( ) ;
74- await typeText ( stdin , 'second' ) ;
75- stdin . write ( ENTER ) ;
139+ await typeText ( rerender , 'second' , chat ) ;
140+ submitInput ( 'second' ) ;
141+ rerender ( chat ) ;
76142 await waitForStream ( ) ;
77143 const frame = lastFrame ( ) ?? '' ;
78144 const firstIdx = frame . indexOf ( 'first' ) ;
@@ -83,36 +149,46 @@ describe('Chat', () => {
83149} ) ;
84150
85151describe ( 'Chat with error' , ( ) => {
152+ beforeEach ( ( ) => {
153+ mockState . clear ( ) ;
154+ } ) ;
155+
86156 it ( 'shows error message when stream fails with Error' , async ( ) => {
87- const { streamChat } = await import ( '../utils/ollama' ) ;
157+ const { ollama } = await import ( '../utils' ) ;
158+ const { streamChat } = ollama ;
88159 vi . mocked ( streamChat ) . mockImplementationOnce ( async function * ( ) {
89160 await Promise . resolve ( ) ;
90161 yield '' ;
91162 throw new Error ( 'Connection failed' ) ;
92163 } ) ;
93164
94- const { lastFrame, stdin } = render ( < Chat /> ) ;
165+ const chat = < Chat /> ;
166+ const { lastFrame, rerender } = render ( chat ) ;
95167
96- await typeText ( stdin , 'hello' ) ;
97- stdin . write ( ENTER ) ;
168+ await typeText ( rerender , 'hello' , chat ) ;
169+ submitInput ( 'hello' ) ;
170+ rerender ( chat ) ;
98171 await waitForStream ( ) ;
99172
100173 expect ( lastFrame ( ) ) . toContain ( 'Error: Connection failed' ) ;
101174 } ) ;
102175
103176 it ( 'shows error message when stream fails with non-Error' , async ( ) => {
104- const { streamChat } = await import ( '../utils/ollama' ) ;
177+ const { ollama } = await import ( '../utils' ) ;
178+ const { streamChat } = ollama ;
105179 vi . mocked ( streamChat ) . mockImplementationOnce ( async function * ( ) {
106180 await Promise . resolve ( ) ;
107181 yield '' ;
108182 // eslint-disable-next-line @typescript-eslint/only-throw-error
109183 throw { toString : ( ) => 'Custom error' } ;
110184 } ) ;
111185
112- const { lastFrame, stdin } = render ( < Chat /> ) ;
186+ const chat = < Chat /> ;
187+ const { lastFrame, rerender } = render ( chat ) ;
113188
114- await typeText ( stdin , 'hello' ) ;
115- stdin . write ( ENTER ) ;
189+ await typeText ( rerender , 'hello' , chat ) ;
190+ submitInput ( 'hello' ) ;
191+ rerender ( chat ) ;
116192 await waitForStream ( ) ;
117193
118194 expect ( lastFrame ( ) ) . toContain ( 'Error: Custom error' ) ;
0 commit comments