diff --git a/.changeset/windows-prompt-underline.md b/.changeset/windows-prompt-underline.md new file mode 100644 index 00000000000..daa7b2a0f74 --- /dev/null +++ b/.changeset/windows-prompt-underline.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +Fix text prompt underlines rendering as unsupported characters in Windows consoles. diff --git a/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.test.tsx b/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.test.tsx index b5eb3fc0d7e..69ac5628d5b 100644 --- a/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.test.tsx +++ b/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.test.tsx @@ -1,14 +1,37 @@ import {DangerousConfirmationPrompt} from './DangerousConfirmationPrompt.js' import {getLastFrameAfterUnmount, sendInputAndWaitForChange, waitForInputsToBeReady, render} from '../../testing/ui.js' import {unstyled} from '../../../../public/node/output.js' +import colors from '../../../../public/node/colors.js' +import {platformAndArch} from '../../../../public/node/os.js' import React from 'react' -import {describe, expect, test, vi} from 'vitest' +import {beforeEach, describe, expect, test, vi} from 'vitest' + +vi.mock('../../../../public/node/os.js') + +beforeEach(() => { + vi.mocked(platformAndArch).mockReturnValue({platform: 'darwin', arch: 'arm64'}) +}) const ENTER = '\r' const ESC = '\x1b' describe('DangerousConfirmationPrompt', () => { + test.each(['windows', 'darwin', 'linux'] as const)('renders the underline on %s', async (platform) => { + vi.mocked(platformAndArch).mockReturnValue({platform, arch: 'amd64'}) + const renderInstance = render( + {}} message="Test question" confirmation="yes" />, + ) + const underline = (platform === 'windows' ? '─' : '▔').repeat(77) + + expect(renderInstance.lastFrame()).toContain(colors.cyan(underline)) + + await waitForInputsToBeReady() + await sendInputAndWaitForChange(renderInstance, ENTER) + + expect(renderInstance.lastFrame()).toContain(colors.red(underline)) + }) + test('default state', () => { const {lastFrame} = render( {}} message="Test question" confirmation="yes" />, diff --git a/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx b/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx index dfeda4abbe5..73abcf5fe25 100644 --- a/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx +++ b/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx @@ -5,6 +5,7 @@ import {handleCtrlC, useComplete} from '../../ui.js' import useLayout from '../hooks/use-layout.js' import {messageWithPunctuation} from '../utilities.js' import {AbortSignal} from '../../../../public/node/abort.js' +import {platformAndArch} from '../../../../public/node/os.js' import useAbortSignal from '../hooks/use-abort-signal.js' import usePrompt, {PromptState} from '../hooks/use-prompt.js' @@ -44,7 +45,6 @@ const DangerousConfirmationPrompt: FunctionComponent | undefined>(undefined) const color = promptState === PromptState.Error ? 'red' : 'cyan' - const underline = new Array(oneThird - 3).fill('▔') const {isAborted} = useAbortSignal(abortSignal) useInput((input, key) => { @@ -133,9 +133,20 @@ const DangerousConfirmationPrompt: FunctionComponent - - {underline} - + {platformAndArch().platform === 'windows' ? ( + + ) : ( + + {'▔'.repeat(oneThird - 3)} + + )} {promptState === PromptState.Error && error ? ( diff --git a/packages/cli-kit/src/private/node/ui/components/TextPrompt.test.tsx b/packages/cli-kit/src/private/node/ui/components/TextPrompt.test.tsx index 939cec7006e..b41b31b0842 100644 --- a/packages/cli-kit/src/private/node/ui/components/TextPrompt.test.tsx +++ b/packages/cli-kit/src/private/node/ui/components/TextPrompt.test.tsx @@ -3,13 +3,33 @@ import {getLastFrameAfterUnmount, sendInputAndWaitForChange, waitForInputsToBeRe import {unstyled} from '../../../../public/node/output.js' import {AbortController} from '../../../../public/node/abort.js' import colors from '../../../../public/node/colors.js' +import {platformAndArch} from '../../../../public/node/os.js' import React from 'react' -import {describe, expect, test, vi} from 'vitest' +import {beforeEach, describe, expect, test, vi} from 'vitest' + +vi.mock('../../../../public/node/os.js') + +beforeEach(() => { + vi.mocked(platformAndArch).mockReturnValue({platform: 'darwin', arch: 'arm64'}) +}) const ENTER = '\r' describe('TextPrompt', () => { + test.each(['windows', 'darwin', 'linux'] as const)('renders the underline on %s', async (platform) => { + vi.mocked(platformAndArch).mockReturnValue({platform, arch: 'amd64'}) + const renderInstance = render( {}} message="Test question" />) + const underline = (platform === 'windows' ? '─' : '▔').repeat(77) + + expect(renderInstance.lastFrame()).toContain(colors.cyan(underline)) + + await waitForInputsToBeReady() + await sendInputAndWaitForChange(renderInstance, ENTER) + + expect(renderInstance.lastFrame()).toContain(colors.red(underline)) + }) + test('default state', () => { const {lastFrame} = render( {}} message="Test question" defaultValue="Placeholder" />) diff --git a/packages/cli-kit/src/private/node/ui/components/TextPrompt.tsx b/packages/cli-kit/src/private/node/ui/components/TextPrompt.tsx index 37e24221c94..1ca5f5a4749 100644 --- a/packages/cli-kit/src/private/node/ui/components/TextPrompt.tsx +++ b/packages/cli-kit/src/private/node/ui/components/TextPrompt.tsx @@ -4,6 +4,7 @@ import {handleCtrlC, useComplete} from '../../ui.js' import useLayout from '../hooks/use-layout.js' import {messageWithPunctuation} from '../utilities.js' import {AbortSignal} from '../../../../public/node/abort.js' +import {platformAndArch} from '../../../../public/node/os.js' import useAbortSignal from '../hooks/use-abort-signal.js' import usePrompt, {PromptState} from '../hooks/use-prompt.js' import React, {FunctionComponent, useCallback, useEffect, useState} from 'react' @@ -64,7 +65,6 @@ const TextPrompt: FunctionComponent = ({ const complete = useComplete() const [error, setError] = useState(undefined) const color = promptState === PromptState.Error ? 'red' : 'cyan' - const underline = new Array(oneThird - 3).fill('▔') const {isAborted} = useAbortSignal(abortSignal) useInput((input, key) => { @@ -128,9 +128,20 @@ const TextPrompt: FunctionComponent = ({ /> - - {underline} - + {platformAndArch().platform === 'windows' ? ( + + ) : ( + + {'▔'.repeat(oneThird - 3)} + + )} {promptState === PromptState.Error ? ( {error}