@@ -2,6 +2,7 @@ package prompter
22
33import (
44 "io"
5+ "sync"
56 "testing"
67 "time"
78
@@ -18,8 +19,9 @@ import (
1819// bubbletea event loop.
1920
2021type interactionStep struct {
21- bytes []byte
22- delay time.Duration // pause before sending (lets the event loop settle)
22+ bytes []byte
23+ delay time.Duration // pause before sending (lets the event loop settle)
24+ waitFn func () // if non-nil, called instead of time.Sleep(delay)
2325}
2426
2527type interaction struct {
@@ -33,9 +35,15 @@ func newInteraction(steps ...interactionStep) interaction {
3335func (ix interaction ) run (t * testing.T , w * io.PipeWriter ) {
3436 t .Helper ()
3537 for _ , s := range ix .steps {
36- time .Sleep (s .delay )
37- _ , err := w .Write (s .bytes )
38- require .NoError (t , err )
38+ if s .waitFn != nil {
39+ s .waitFn ()
40+ } else {
41+ time .Sleep (s .delay )
42+ }
43+ if s .bytes != nil {
44+ _ , err := w .Write (s .bytes )
45+ require .NoError (t , err )
46+ }
3947 }
4048}
4149
@@ -98,11 +106,45 @@ func clearLine() interactionStep {
98106 return interactionStep {bytes : []byte {0x01 , 0x0b }}
99107}
100108
101- // waitForOptions adds extra delay to let OptionsFunc load before continuing.
109+ // waitForOptions adds extra delay to let the bubbletea event loop settle
110+ // after switching modes when no async search is triggered.
102111func waitForOptions () interactionStep {
103112 return interactionStep {bytes : nil , delay : 50 * time .Millisecond }
104113}
105114
115+ // waitForSearch returns an interactionStep that blocks until the field's
116+ // current async search completes. It wires a one-shot callback into the
117+ // field's onSearchDone hook and waits for it to fire, avoiding fixed-duration
118+ // sleeps that are too short on slow architectures such as s390x under QEMU.
119+ //
120+ // The wait is bounded by the test's deadline (from -timeout) so a hung search
121+ // fails the test with a clear message rather than blocking the whole test run.
122+ func waitForSearch (t * testing.T , field * multiSelectSearchField ) interactionStep {
123+ t .Helper ()
124+ done := make (chan struct {})
125+ var once sync.Once
126+ field .onSearchDone .Store (func () {
127+ once .Do (func () { close (done ) })
128+ })
129+
130+ var timeout <- chan time.Time
131+ if deadline , ok := t .Deadline (); ok {
132+ timeout = time .After (time .Until (deadline ))
133+ } else {
134+ timeout = time .After (30 * time .Second )
135+ }
136+
137+ return interactionStep {
138+ waitFn : func () {
139+ select {
140+ case <- done :
141+ case <- timeout :
142+ t .Fatal ("timed out waiting for async search to complete" )
143+ }
144+ },
145+ }
146+ }
147+
106148// --- Test harness ---
107149
108150func newTestHuhPrompter () * huhPrompter {
@@ -475,13 +517,16 @@ func TestHuhPrompterMultiSelectWithSearchPersistence(t *testing.T) {
475517 f , result := p .buildMultiSelectWithSearchForm (
476518 "Select" , "Search" , nil , nil , staticSearchFunc ,
477519 )
520+ // waitForSearch must be created before runForm so the hook is in place
521+ // before the async search fires.
522+ searchDone := waitForSearch (t , result )
478523 runForm (t , f , newInteraction (
479- tab (), waitForOptions (),
480- toggle (), // toggle result-a
481- shiftTab (), // back to search input
482- typeKeys ("foo" ), // change query
483- tab (), waitForOptions (),
484- enter (), // submit — result-a should persist
524+ tab (), waitForOptions (), // switch to select mode (no async search)
525+ toggle (), // toggle result-a
526+ shiftTab (), // back to search input
527+ typeKeys ("foo" ), // change query
528+ tab (), searchDone , // submit query → async search; wait for completion
529+ enter (), // submit form — guaranteed search is done
485530 ))
486531 assert .Equal (t , []string {"result-a" }, result .selectedKeys ())
487532 })
0 commit comments