Fix flaky TextInput and AutocompletePrompt tests under React 19#6922
Conversation
Coverage report
Test suite run success3785 tests passing in 1448 suites. Report generated by 🧪jest coverage report action from c855e17 |
|
/snapit |
|
🫰✨ Thanks @tewson! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260302130653Caution After installing, validate the version by running |
|
We detected some changes at Caution DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release. |
bec41c5 to
6cd5658
Compare
5f084c2 to
662837c
Compare
|
/snapit |
|
🫰✨ Thanks @tewson! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260302153804Caution After installing, validate the version by running |
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>
662837c to
4518206
Compare
| const clampedCursorOffset = Math.min(cursorOffset, (originalValue || '').length) | ||
| if (clampedCursorOffset !== cursorOffset) { | ||
| setCursorOffset(clampedCursorOffset) | ||
| } |
There was a problem hiding this comment.
Render-phase state update (setCursorOffset) may cause React warnings or render loops
TextInput clamps cursor offset by calling setCursorOffset synchronously during render:
const clampedCursorOffset = Math.min(cursorOffset, (originalValue || '').length)
if (clampedCursorOffset !== cursorOffset) {
setCursorOffset(clampedCursorOffset)
}Updating state during render is an anti-pattern and can cause React warnings/errors (e.g., "Cannot update a component while rendering..."), extra render passes, and potential render loops in edge cases. It’s also more unpredictable under React 18/19 concurrent scheduling—exactly the environment this PR is targeting. The goal (avoid stale offsets reaching useInput) is valid, but can be achieved without render-phase mutation by using the clamped value for computations and syncing state in an effect/layout effect.
There was a problem hiding this comment.
Is this relevant @tewson ? or can we ignore it?
There was a problem hiding this comment.
Supposedly this can be ignored because this version of Ink doesn't use concurrent mode. But newer versions support concurrent mode so I made a change in c855e17 to make it safer.
|
🤖 Code Review · #projects-dev-ai for questions ✅ Complete - No issues 📋 History✅ 1 findings → ✅ No issues |
|
/snapit |
|
🫰✨ Thanks @tewson! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260302160425Caution After installing, validate the version by running |
|
Tested with |
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationsWe found no new type declarations in this PR Existing type declarationspackages/cli-kit/dist/private/node/testing/ui.d.ts@@ -73,7 +73,8 @@ export declare function sendInputAndWait(renderInstance: ReturnType<typeof rende
export declare function sendInputAndWaitForContent(renderInstance: ReturnType<typeof render>, content: string, ...inputs: string[]): Promise<void>;
/** Function that is useful when you want to check the last frame of a component that unmounted.
*
- * The reason this function exists is that in CI Ink will clear the last frame on unmount.
+ * With Ink 6 / React 19, the output is no longer cleared on unmount,
+ * so lastFrame() consistently returns the last rendered content.
*/
export declare function getLastFrameAfterUnmount(renderInstance: ReturnType<typeof render>): string | undefined;
type TrackedPromise<T> = Promise<T> & {
packages/cli-kit/dist/private/node/ui/components/SelectInput.d.ts@@ -1,8 +1,5 @@
import React from 'react';
import { DOMElement } from 'ink';
-declare module 'react' {
- function forwardRef<T, TProps>(render: (props: TProps, ref: React.Ref<T>) => React.ReactElement | null): (props: TProps & React.RefAttributes<T>) => React.ReactElement | null;
-}
export interface SelectInputProps<T> {
items: Item<T>[];
initialItems?: Item<T>[];
@@ -18,7 +15,8 @@ export interface SelectInputProps<T> {
morePagesMessage?: string;
availableLines?: number;
onSubmit?: (item: Item<T>) => void;
- inputFixedAreaRef?: React.RefObject<DOMElement>;
+ inputFixedAreaRef?: React.Ref<DOMElement>;
+ ref?: React.Ref<DOMElement>;
groupOrder?: string[];
}
export interface Item<T> {
@@ -29,4 +27,5 @@ export interface Item<T> {
helperText?: string;
disabled?: boolean;
}
-export declare const SelectInput: <T>(props: SelectInputProps<T> & React.RefAttributes<DOMElement>) => React.ReactElement | null;
\ No newline at end of file
+declare function SelectInput<T>({ items: rawItems, initialItems, onChange, enableShortcuts, focus, emptyMessage, defaultValue, highlightedTerm, loading, errorMessage, hasMorePages, morePagesMessage, availableLines, onSubmit, inputFixedAreaRef, ref, groupOrder, }: SelectInputProps<T>): React.ReactElement | null;
+export { SelectInput };
\ No newline at end of file
|
|
/snapit |
|
🫰✨ Thanks @tewson! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260303113119Caution After installing, validate the version by running |
isaacroldan
left a comment
There was a problem hiding this comment.
tophatted and everything works as expected
Summary
useEffectcursor clamping inTextInputwith synchronous clamping during render to eliminate a stalecursorOffsetrace condition exposed by React 19's scheduler changesAutocompletePromptDELETE-related test assertions wait for deterministic content (sendInputAndWaitForContent) instead of any frame change (sendInputAndWaitForChange)Test plan
TextInput.test.tsx— all 12 tests passAutocompletePrompt.test.tsx— all 13 tests pass🤖 Generated with Claude Code