diff --git a/bun.lock b/bun.lock index 9077240f..93cdd841 100644 --- a/bun.lock +++ b/bun.lock @@ -43,7 +43,6 @@ "dotenv": "^16.6.1", "eslint": "^9", "eslint-config-prettier": "^10", - "react-test-renderer": "^19.2.0", "shx": "^0.3.3", "ts-node": "^10", "tsx": "^4.20.6", @@ -1519,8 +1518,6 @@ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], - "react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], - "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], @@ -1533,8 +1530,6 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "react-test-renderer": ["react-test-renderer@19.2.3", "", { "dependencies": { "react-is": "^19.2.3", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw=="], - "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="], "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], diff --git a/cli/package.json b/cli/package.json index 2a4d6854..0d02f2ce 100644 --- a/cli/package.json +++ b/cli/package.json @@ -65,8 +65,7 @@ "ts-node": "^10", "tsx": "^4.20.6", "typescript": "^5", - "vitest": "^3.2.4", - "react-test-renderer": "^19.2.0" + "vitest": "^3.2.4" }, "resolutions": { "whatwg-url": "14.2.0", diff --git a/cli/src/ui/ink/useSwitchControls.test.ts b/cli/src/ui/ink/useSwitchControls.test.ts index 7612071d..d9ef1542 100644 --- a/cli/src/ui/ink/useSwitchControls.test.ts +++ b/cli/src/ui/ink/useSwitchControls.test.ts @@ -1,5 +1,6 @@ -import React, { useEffect } from 'react'; -import TestRenderer, { act, type ReactTestRenderer } from 'react-test-renderer'; +import React, { act, useEffect } from 'react'; +import { PassThrough } from 'node:stream'; +import { render, type Instance } from 'ink'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useSwitchControls, type ConfirmationMode, type ActionInProgress } from './useSwitchControls'; @@ -16,11 +17,48 @@ type SwitchState = { let inputHandler: ((input: string, key: Key) => void | Promise) | null = null; -vi.mock('ink', () => ({ - useInput: (handler: (input: string, key: Key) => void | Promise) => { - inputHandler = handler; - } -})); +vi.mock('ink', async () => { + const actual = await vi.importActual('ink'); + return { + ...actual, + useInput: (handler: (input: string, key: Key) => void | Promise) => { + inputHandler = handler; + } + }; +}); + +const createInkStreams = (): { + stdout: NodeJS.WriteStream; + stderr: NodeJS.WriteStream; + stdin: NodeJS.ReadStream; +} => { + const stdout = new PassThrough() as NodeJS.WriteStream & { + isTTY?: boolean; + columns?: number; + rows?: number; + }; + const stderr = new PassThrough() as NodeJS.WriteStream & { + isTTY?: boolean; + columns?: number; + rows?: number; + }; + const stdin = new PassThrough() as NodeJS.ReadStream & { + isTTY?: boolean; + }; + + Object.assign(stdout, { isTTY: true, columns: 80, rows: 24 }); + Object.assign(stderr, { isTTY: true, columns: 80, rows: 24 }); + Object.assign(stdin, { isTTY: false }); + + return { stdout, stderr, stdin }; +}; + +const getActEnvironment = (): boolean | undefined => + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT; + +const setActEnvironment = (value: boolean | undefined) => { + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = value; +}; function HookProbe(props: { onExit?: () => void; @@ -41,18 +79,27 @@ function HookProbe(props: { } describe('useSwitchControls', () => { - let renderer: ReactTestRenderer | null = null; + let renderer: Instance | null = null; let latestState: SwitchState | null = null; + let previousActEnvironment: boolean | undefined; const mount = async (opts: { onExit?: () => void; onSwitch?: () => void }) => { + const { stdout, stderr, stdin } = createInkStreams(); await act(async () => { - renderer = TestRenderer.create( + renderer = render( React.createElement(HookProbe, { ...opts, onState: (state) => { latestState = state; } - }) + }), + { + stdout, + stderr, + stdin, + exitOnCtrlC: false, + patchConsole: false + } ); }); }; @@ -84,20 +131,26 @@ describe('useSwitchControls', () => { }; beforeEach(() => { + previousActEnvironment = getActEnvironment(); + setActEnvironment(true); vi.useFakeTimers(); inputHandler = null; latestState = null; }); - afterEach(() => { + afterEach(async () => { + await act(async () => { + vi.runOnlyPendingTimers(); + }); if (renderer) { - act(() => { - renderer?.unmount(); + await act(async () => { + renderer.unmount(); }); + renderer.cleanup(); renderer = null; } - vi.runOnlyPendingTimers(); vi.useRealTimers(); + setActEnvironment(previousActEnvironment); }); it('forwards Ctrl-C to process when onExit is missing', async () => {