Skip to content

Commit 4518206

Browse files
tewsonclaude
andcommitted
Fix flaky TextInput and AutocompletePrompt tests under React 19
Replace async useEffect cursor clamping in TextInput with synchronous clamping during render to eliminate stale cursorOffset race condition exposed by React 19 scheduler changes. Also make AutocompletePrompt DELETE tests wait for deterministic content instead of any frame change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 520e572 commit 4518206

File tree

2 files changed

+20
-23
lines changed

2 files changed

+20
-23
lines changed

packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ describe('AutocompletePrompt', async () => {
448448
"
449449
`)
450450

451-
await sendInputAndWaitForChange(renderInstance, DELETE)
451+
await sendInputAndWaitForContent(renderInstance, 'ype to search', DELETE)
452452

453453
expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
454454
"? Associate your project with the org Castile Ventures? Type to search...
@@ -697,7 +697,7 @@ describe('AutocompletePrompt', async () => {
697697
"
698698
`)
699699

700-
await sendInputAndWaitForChange(renderInstance, DELETE)
700+
await sendInputAndWaitForContent(renderInstance, 'ype to search', DELETE)
701701

702702
expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
703703
"? Associate your project with the org Castile Ventures? Type to search...

packages/cli-kit/src/private/node/ui/components/TextInput.tsx

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-nested-ternary */
22
import {shouldDisplayColors} from '../../../../public/node/output.js'
3-
import React, {useEffect, useState} from 'react'
3+
import React, {useState} from 'react'
44
import {Text, useInput} from 'ink'
55
import chalk from 'chalk'
66
import figures from 'figures'
@@ -29,18 +29,11 @@ const TextInput: FunctionComponent<TextInputProps> = ({
2929
}: TextInputProps) => {
3030
const [cursorOffset, setCursorOffset] = useState((originalValue || '').length)
3131

32-
// if the updated value is shorter than the last one we need to reset the cursor
33-
useEffect(() => {
34-
setCursorOffset((previousOffset) => {
35-
const newValue = originalValue || ''
36-
37-
if (previousOffset > newValue.length - 1) {
38-
return newValue.length
39-
}
40-
41-
return previousOffset
42-
})
43-
}, [originalValue])
32+
// Clamp cursor synchronously so useInput never sees a stale offset
33+
const clampedCursorOffset = Math.min(cursorOffset, (originalValue || '').length)
34+
if (clampedCursorOffset !== cursorOffset) {
35+
setCursorOffset(clampedCursorOffset)
36+
}
4437

4538
const value = password ? '*'.repeat(originalValue.length) : originalValue
4639
let renderedValue
@@ -60,15 +53,15 @@ const TextInput: FunctionComponent<TextInputProps> = ({
6053
renderedValue = value
6154
.split('')
6255
.map((char, index) => {
63-
if (index === cursorOffset) {
56+
if (index === clampedCursorOffset) {
6457
return noColor ? cursorChar : chalk.inverse(char)
6558
} else {
6659
return char
6760
}
6861
})
6962
.join('')
7063

71-
if (cursorOffset === value.length) {
64+
if (clampedCursorOffset === value.length) {
7265
renderedValue = (
7366
<Text>
7467
{renderedValue}
@@ -89,25 +82,29 @@ const TextInput: FunctionComponent<TextInputProps> = ({
8982
}
9083
}
9184

92-
let nextCursorOffset = cursorOffset
85+
let nextCursorOffset = clampedCursorOffset
9386
let nextValue = originalValue
9487

9588
if (key.leftArrow) {
96-
if (cursorOffset > 0) {
89+
if (clampedCursorOffset > 0) {
9790
nextCursorOffset--
9891
}
9992
} else if (key.rightArrow) {
100-
if (cursorOffset < originalValue.length) {
93+
if (clampedCursorOffset < originalValue.length) {
10194
nextCursorOffset++
10295
}
10396
} else if (key.backspace || key.delete) {
104-
if (cursorOffset > 0) {
105-
nextValue = originalValue.slice(0, cursorOffset - 1) + originalValue.slice(cursorOffset, originalValue.length)
97+
if (clampedCursorOffset > 0) {
98+
nextValue =
99+
originalValue.slice(0, clampedCursorOffset - 1) +
100+
originalValue.slice(clampedCursorOffset, originalValue.length)
106101
nextCursorOffset--
107102
}
108103
} else {
109104
nextValue =
110-
originalValue.slice(0, cursorOffset) + input + originalValue.slice(cursorOffset, originalValue.length)
105+
originalValue.slice(0, clampedCursorOffset) +
106+
input +
107+
originalValue.slice(clampedCursorOffset, originalValue.length)
111108
nextCursorOffset += input.length
112109
}
113110

0 commit comments

Comments
 (0)