From 04f387f10b793c59d8da669d2e288b030e486155 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sun, 26 Jul 2026 09:10:48 +0100 Subject: [PATCH 1/2] test(box): remove redundant test This was added in #588 but isn't actually a meaningful test. The existing `box` tests already assert that the guide is correct through snapshots (the snapshots were just wrong previously, i.e. we missed it, it was a bug). These two prompts don't actually need to be bound to each other. --- packages/prompts/test/box.test.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/prompts/test/box.test.ts b/packages/prompts/test/box.test.ts index 4163c7b5..54378210 100644 --- a/packages/prompts/test/box.test.ts +++ b/packages/prompts/test/box.test.ts @@ -98,23 +98,6 @@ describe.each(['true', 'false'])('box (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); - test('renders the guide in the same style as log', () => { - const logOutput = new MockWritable(); - prompts.log.info('message', { input, output: logOutput }); - const [logGuide] = logOutput.buffer.join('').split('\n'); - - prompts.box('message', 'title', { - input, - output, - width: 'auto', - formatBorder: (text) => styleText('green', text), - }); - - for (const line of output.buffer) { - expect(line.startsWith(`${logGuide} `)).toBe(true); - } - }); - test('renders without guide when withGuide is false', () => { prompts.box('message', 'title', { input, From 69d4720a487e0533df55e28b0094b71cfed630c3 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:21:44 +0100 Subject: [PATCH 2/2] chore: add extra layer of testing --- packages/prompts/test/guide.test.ts | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/prompts/test/guide.test.ts diff --git a/packages/prompts/test/guide.test.ts b/packages/prompts/test/guide.test.ts new file mode 100644 index 00000000..2da655f0 --- /dev/null +++ b/packages/prompts/test/guide.test.ts @@ -0,0 +1,89 @@ +import { updateSettings } from '@clack/core'; +import { afterEach, describe, expect, test } from 'vitest'; +import { type CommonOptions, S_BAR, S_BAR_END } from '../src/common.js'; +import * as prompts from '../src/index.js'; +import { MockReadable, MockWritable } from './test-utils.js'; + +const MESSAGE = 'message'; +const OPTIONS = [{ value: 'a' }, { value: 'b' }]; + +const factories = { + text: (opts: CommonOptions) => prompts.text({ message: MESSAGE, ...opts }), + password: (opts: CommonOptions) => prompts.password({ message: MESSAGE, ...opts }), + confirm: (opts: CommonOptions) => prompts.confirm({ message: MESSAGE, ...opts }), + multiline: (opts: CommonOptions) => prompts.multiline({ message: MESSAGE, ...opts }), + date: (opts: CommonOptions) => prompts.date({ message: MESSAGE, ...opts }), + path: (opts: CommonOptions) => prompts.path({ message: MESSAGE, ...opts }), + select: (opts: CommonOptions) => prompts.select({ message: MESSAGE, options: OPTIONS, ...opts }), + selectKey: (opts: CommonOptions) => + prompts.selectKey({ message: MESSAGE, options: OPTIONS, ...opts }), + multiselect: (opts: CommonOptions) => + prompts.multiselect({ message: MESSAGE, options: OPTIONS, ...opts }), + groupMultiselect: (opts: CommonOptions) => + prompts.groupMultiselect({ message: MESSAGE, options: { group: OPTIONS }, ...opts }), + autocomplete: (opts: CommonOptions) => + prompts.autocomplete({ message: MESSAGE, options: OPTIONS, ...opts }), + autocompleteMultiselect: (opts: CommonOptions) => + prompts.autocompleteMultiselect({ message: MESSAGE, options: OPTIONS, ...opts }), +} satisfies Record Promise>; + +type PromptName = keyof typeof factories; + +const promptNames = Object.keys(factories) as PromptName[]; + +function firstGuide(frame: string): string { + for (const line of frame.split('\n')) { + // biome-ignore lint/suspicious/noControlCharactersInRegex: matching ANSI escapes + const match = /^\x1b\[(\d+)m(.)/.exec(line); + + if (match && (match[2] === S_BAR || match[2] === S_BAR_END)) { + return `${match[1]} ${match[2]}`; + } + } + + return 'none'; +} + +async function renderGuide( + name: PromptName, + opts: Omit = {} +): Promise { + const input = new MockReadable(); + const output = new MockWritable(); + const result = factories[name]({ input, output, ...opts }); + + input.emit('keypress', 'escape', { name: 'escape' }); + await result; + + return firstGuide(output.buffer[1] ?? ''); +} + +describe('guide', () => { + afterEach(() => { + updateSettings({ withGuide: true }); + }); + + test('every prompt renders the same guide', async () => { + const guides: Record = {}; + + for (const name of promptNames) { + guides[name] = await renderGuide(name); + } + + expect(guides).toEqual(Object.fromEntries(promptNames.map((name) => [name, `90 ${S_BAR}`]))); + }); + + test('no prompt renders a guide when withGuide is false', async () => { + for (const name of promptNames) { + expect(await renderGuide(name, { withGuide: false }), name).toBe('none'); + } + }); + + test('no prompt renders a guide when withGuide is globally false', async () => { + updateSettings({ withGuide: false }); + + for (const name of promptNames) { + expect(await renderGuide(name), name).toBe('none'); + } + }); +});