From 599317f9e41c65bcb9c790264dec4b47bd7d9f43 Mon Sep 17 00:00:00 2001 From: Tewson Seeoun Date: Tue, 3 Mar 2026 12:22:11 +0000 Subject: [PATCH] Fix flaky AutocompletePrompt tests under React 19 on CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the Stdin mock to re-emit 'readable' when a listener is added and there's pending data. This mirrors real Node stream behavior and prevents dropped input when data is written before Ink's useInput effect registers its listener — the root cause of the "allows searching with pagination" timeout on slow CI runners. Replace sendInputAndWait fixed delays with sendInputAndWaitForContent in the "no submit" tests, which deterministically waits for React to confirm the frame still shows expected content after the keypress. Use a never-resolving search promise in the loading state test so the loading state persists for the entire test, eliminating races with React 19's batched rendering. Co-Authored-By: Claude Opus 4.6 --- .../cli-kit/src/private/node/testing/ui.ts | 11 ++++++++++ .../ui/components/AutocompletePrompt.test.tsx | 21 +++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/cli-kit/src/private/node/testing/ui.ts b/packages/cli-kit/src/private/node/testing/ui.ts index 13c5019050..66d47e1144 100644 --- a/packages/cli-kit/src/private/node/testing/ui.ts +++ b/packages/cli-kit/src/private/node/testing/ui.ts @@ -23,6 +23,17 @@ export class Stdin extends EventEmitter { constructor(options: {isTTY?: boolean} = {}) { super() this.isTTY = options.isTTY ?? true + + // When a 'readable' listener is added and there's pending data, + // re-emit 'readable' so the new listener can read it. This mirrors + // real Node streams where buffered data is available to new readers, + // and prevents dropped input when data is written before Ink's + // useInput effect registers its listener. + this.on('newListener', (event: string) => { + if (event === 'readable' && this.data !== null) { + setImmediate(() => this.emit('readable')) + } + }) } write = (data: string) => { diff --git a/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx b/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx index 8e04bb76d7..b097057eca 100644 --- a/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx +++ b/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx @@ -1,7 +1,6 @@ import {AutocompletePrompt, SearchResults} from './AutocompletePrompt.js' import { getLastFrameAfterUnmount, - sendInputAndWait, sendInputAndWaitForChange, sendInputAndWaitForContent, waitForInputsToBeReady, @@ -286,8 +285,9 @@ describe('AutocompletePrompt', async () => { await waitForInputsToBeReady() await sendInputAndWaitForContent(renderInstance, 'No results found', 'a') - // prompt doesn't change when enter is pressed - await sendInputAndWait(renderInstance, 100, ENTER) + // prompt doesn't change when enter is pressed — yield to let React + // 19's scheduler process any batched updates from the keypress + await sendInputAndWaitForContent(renderInstance, 'No results found', ENTER) expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? a█ @@ -328,12 +328,11 @@ describe('AutocompletePrompt', async () => { test('has a loading state', async () => { const onEnter = vi.fn() + // Use a promise that never resolves so the loading state persists + // for the entire test, eliminating timing races with React 19's + // batched rendering on slow CI runners. const search = () => { - return new Promise>((resolve) => { - setTimeout(() => { - resolve({data: [{label: 'a', value: 'b'}]}) - }, 2000) - }) + return new Promise>(() => {}) } const renderInstance = render( @@ -347,9 +346,9 @@ describe('AutocompletePrompt', async () => { await waitForInputsToBeReady() await sendInputAndWaitForContent(renderInstance, 'Loading...', 'a') - // prompt doesn't change when enter is pressed - await new Promise((resolve) => setTimeout(resolve, 100)) - await sendInputAndWait(renderInstance, 100, ENTER) + // prompt doesn't change when enter is pressed — yield to let React + // 19's scheduler process any batched updates from the keypress + await sendInputAndWaitForContent(renderInstance, 'Loading...', ENTER) expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? a█