2026-05-13 18:41:44 -04:00
|
|
|
|
/* @jsxImportSource react */
|
|
|
|
|
|
import { render as renderInkTest } from 'ink-testing-library';
|
|
|
|
|
|
import { type ReactNode } from 'react';
|
|
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
|
|
import { buildInitialState, buildPickerTree, type TreePickerNodeInput } from './tree-picker-state.js';
|
|
|
|
|
|
import {
|
|
|
|
|
|
TreePickerApp,
|
|
|
|
|
|
renderTreePickerTui,
|
|
|
|
|
|
resolveTreePickerWidth,
|
|
|
|
|
|
sanitizeTreePickerTuiError,
|
|
|
|
|
|
treePickerCommandForInkInput,
|
|
|
|
|
|
windowItems,
|
|
|
|
|
|
windowOffset,
|
|
|
|
|
|
type TreePickerChrome,
|
|
|
|
|
|
type TreePickerInkInstance,
|
|
|
|
|
|
type TreePickerInkRenderOptions,
|
|
|
|
|
|
} from './tree-picker-tui.js';
|
|
|
|
|
|
|
|
|
|
|
|
const IDS = {
|
|
|
|
|
|
engineering: '11111111-1111-1111-1111-111111111111',
|
|
|
|
|
|
architecture: '22222222-2222-2222-2222-222222222222',
|
|
|
|
|
|
marketing: '33333333-3333-3333-3333-333333333333',
|
|
|
|
|
|
finance: '44444444-4444-4444-4444-444444444444',
|
|
|
|
|
|
ops: '55555555-5555-5555-5555-555555555555',
|
|
|
|
|
|
sales: '66666666-6666-6666-6666-666666666666',
|
|
|
|
|
|
support: '77777777-7777-7777-7777-777777777777',
|
|
|
|
|
|
product: '88888888-8888-8888-8888-888888888888',
|
|
|
|
|
|
design: '99999999-9999-9999-9999-999999999999',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function pages(): TreePickerNodeInput[] {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ id: IDS.engineering, title: 'Engineering Docs', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.architecture, title: 'Architecture', archived: false, parentId: IDS.engineering },
|
|
|
|
|
|
{ id: IDS.marketing, title: 'Marketing', archived: false, parentId: null },
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function manyPages(): TreePickerNodeInput[] {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ id: IDS.engineering, title: 'Engineering Docs', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.architecture, title: 'Architecture', archived: false, parentId: IDS.engineering },
|
|
|
|
|
|
{ id: IDS.marketing, title: 'Marketing', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.finance, title: 'Finance', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.ops, title: 'Operations', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.sales, title: 'Sales', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.support, title: 'Support', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.product, title: 'Product', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.design, title: 'Design', archived: false, parentId: null },
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function state(options: { requireConfirmOnSave?: boolean } = {}) {
|
|
|
|
|
|
return buildInitialState({
|
|
|
|
|
|
tree: buildPickerTree(pages()),
|
|
|
|
|
|
existingSelectedIds: [],
|
|
|
|
|
|
requireConfirmOnSave: options.requireConfirmOnSave ?? false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function chrome(overrides: Partial<TreePickerChrome> = {}): TreePickerChrome {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: 'Select items',
|
|
|
|
|
|
subtitleLines: ['Source: Test'],
|
|
|
|
|
|
...overrides,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function waitForInkInput(): Promise<void> {
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fakeInkInstance(): TreePickerInkInstance {
|
|
|
|
|
|
return {
|
|
|
|
|
|
rerender: vi.fn(),
|
|
|
|
|
|
unmount: vi.fn(),
|
|
|
|
|
|
waitUntilExit: vi.fn(async () => undefined),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeFrameWrap(frame: string | undefined): string {
|
|
|
|
|
|
return frame?.replace(/\n/g, ' ').replace(/│ /g, '').replace(/ +/g, ' ') ?? '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
describe('treePickerCommandForInkInput', () => {
|
|
|
|
|
|
it('maps browse, search, and confirm input to reducer commands', () => {
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { downArrow: true }, state().search, null)).toBe('cursor-down');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { upArrow: true }, state().search, null)).toBe('cursor-up');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { rightArrow: true }, state().search, null)).toBe('cursor-right');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { leftArrow: true }, state().search, null)).toBe('cursor-left');
|
|
|
|
|
|
expect(treePickerCommandForInkInput(' ', {}, state().search, null)).toBe('toggle-check');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('/', {}, state().search, null)).toBe('search-start');
|
2026-05-14 14:35:58 +02:00
|
|
|
|
expect(treePickerCommandForInkInput('a', {}, state().search, null)).toBe('toggle-select-all-visible');
|
2026-05-13 18:41:44 -04:00
|
|
|
|
expect(treePickerCommandForInkInput('n', {}, state().search, null)).toBe('select-none');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { return: true }, state().search, null)).toBe('save-request');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { escape: true }, state().search, null)).toBe('quit');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('c', { ctrl: true }, state().search, null)).toBe('quit');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('s', {}, state().search, null)).toBeNull();
|
|
|
|
|
|
expect(treePickerCommandForInkInput('q', {}, state().search, null)).toBeNull();
|
|
|
|
|
|
|
|
|
|
|
|
expect(treePickerCommandForInkInput('x', {}, { editing: true, query: '' }, null)).toEqual({
|
|
|
|
|
|
type: 'search-input',
|
|
|
|
|
|
value: 'x',
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { backspace: true }, { editing: true, query: 'x' }, null)).toBe(
|
|
|
|
|
|
'search-backspace',
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { return: true }, { editing: true, query: 'x' }, null)).toBe(
|
|
|
|
|
|
'search-submit',
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { escape: true }, { editing: true, query: 'x' }, null)).toBe(
|
|
|
|
|
|
'search-cancel',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(treePickerCommandForInkInput('y', {}, state().search, 'save-confirm')).toBe('save-confirm');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('', { return: true }, state().search, 'save-confirm')).toBe('save-confirm');
|
|
|
|
|
|
expect(treePickerCommandForInkInput('n', {}, state().search, 'save-confirm')).toBe('save-cancel');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('window helpers', () => {
|
|
|
|
|
|
it('centers the selected row and returns the visible slice', () => {
|
|
|
|
|
|
expect(windowOffset(20, 10, 5)).toBe(8);
|
|
|
|
|
|
expect(windowItems(['a', 'b', 'c', 'd', 'e'], 3, 3)).toEqual({ items: ['c', 'd', 'e'], offset: 2 });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('clamps picker width to the design rule', () => {
|
|
|
|
|
|
expect(resolveTreePickerWidth(200)).toBe(120);
|
|
|
|
|
|
expect(resolveTreePickerWidth(100)).toBe(96);
|
|
|
|
|
|
expect(resolveTreePickerWidth(50)).toBe(60);
|
|
|
|
|
|
expect(resolveTreePickerWidth(undefined)).toBe(96);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('TreePickerApp', () => {
|
|
|
|
|
|
it('renders chrome title, subtitle, warnings, help, and row glyphs', () => {
|
|
|
|
|
|
const initialState = {
|
|
|
|
|
|
...state(),
|
|
|
|
|
|
preLoadWarnings: ['1 stale stored selections - they will be removed if you save'],
|
|
|
|
|
|
};
|
|
|
|
|
|
const { lastFrame } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={initialState}
|
|
|
|
|
|
chrome={chrome({
|
|
|
|
|
|
title: 'Select fancy widgets',
|
|
|
|
|
|
subtitleLines: ['Workspace: Design Workspace'],
|
|
|
|
|
|
warningLines: ['5000-page cap reached - some pages not shown'],
|
|
|
|
|
|
})}
|
|
|
|
|
|
terminalRows={24}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={vi.fn()}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const frame = lastFrame() ?? '';
|
|
|
|
|
|
expect(frame).toContain('Select fancy widgets');
|
|
|
|
|
|
expect(frame).toContain('Workspace: Design Workspace');
|
|
|
|
|
|
expect(frame).toContain('5000-page cap reached - some pages not shown');
|
|
|
|
|
|
expect(frame).toContain('1 stale stored selections - they will be removed if you save');
|
|
|
|
|
|
expect(frame).toContain('◻ Engineering Docs ▸ (1)');
|
|
|
|
|
|
expect(frame).toContain('◻ Marketing');
|
|
|
|
|
|
expect(normalizeFrameWrap(frame)).toContain(
|
|
|
|
|
|
'Right Arrow to expand, Up/Down to move, Space to select or unselect, Slash to filter, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('renders custom help text when supplied', () => {
|
|
|
|
|
|
const { lastFrame } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={state()}
|
|
|
|
|
|
chrome={chrome({ helpText: 'Bespoke instructions here.' })}
|
|
|
|
|
|
terminalRows={24}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={vi.fn()}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(lastFrame() ?? '').toContain('Bespoke instructions here.');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('renders checked parents and locked descendants with locked glyphs', () => {
|
|
|
|
|
|
const initialState = {
|
|
|
|
|
|
...state(),
|
|
|
|
|
|
checked: new Set([IDS.engineering]),
|
|
|
|
|
|
expanded: new Set([IDS.engineering]),
|
|
|
|
|
|
};
|
|
|
|
|
|
const { lastFrame } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={initialState}
|
|
|
|
|
|
chrome={chrome()}
|
|
|
|
|
|
terminalRows={24}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={vi.fn()}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const frame = lastFrame() ?? '';
|
|
|
|
|
|
expect(frame).toContain('◼ Engineering Docs ▾');
|
|
|
|
|
|
expect(frame).toContain(' ◼ Architecture');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-14 14:35:58 +02:00
|
|
|
|
it('renders the partial glyph on a parent whose descendant is checked', () => {
|
|
|
|
|
|
const partialPages: TreePickerNodeInput[] = [
|
|
|
|
|
|
{ id: IDS.engineering, title: 'Engineering Docs', archived: false, parentId: null },
|
|
|
|
|
|
{ id: IDS.architecture, title: 'Architecture', archived: false, parentId: IDS.engineering },
|
|
|
|
|
|
];
|
|
|
|
|
|
const initialState = buildInitialState({
|
|
|
|
|
|
tree: buildPickerTree(partialPages),
|
|
|
|
|
|
existingSelectedIds: [IDS.architecture],
|
|
|
|
|
|
});
|
|
|
|
|
|
const { lastFrame } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={initialState}
|
|
|
|
|
|
chrome={chrome()}
|
|
|
|
|
|
terminalRows={24}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={vi.fn()}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const frame = lastFrame() ?? '';
|
|
|
|
|
|
expect(frame).toContain('◧ Engineering Docs ▾');
|
|
|
|
|
|
expect(frame).toContain(' ◼ Architecture');
|
|
|
|
|
|
expect(frame).not.toContain('◻ Engineering Docs');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-13 18:41:44 -04:00
|
|
|
|
it('supports keyboard selection, confirm-on-save, and save callback', async () => {
|
|
|
|
|
|
const onExit = vi.fn();
|
|
|
|
|
|
const { stdin, lastFrame } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={state({ requireConfirmOnSave: true })}
|
|
|
|
|
|
chrome={chrome({
|
|
|
|
|
|
confirmSaveMessage: (current) =>
|
|
|
|
|
|
`Confirm: ${current.checked.size} item${current.checked.size === 1 ? '' : 's'}? Press Enter or Escape.`,
|
|
|
|
|
|
})}
|
|
|
|
|
|
terminalRows={24}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={onExit}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write(' ');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(lastFrame()).toContain('◼ Engineering Docs');
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('\r');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(normalizeFrameWrap(lastFrame())).toContain('Confirm: 1 item? Press Enter or Escape.');
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('y');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(onExit).toHaveBeenCalledWith({ kind: 'save', selectedIds: [IDS.engineering] });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('uses the chrome-supplied skip-empty message and quits on confirm', async () => {
|
|
|
|
|
|
const onExit = vi.fn();
|
|
|
|
|
|
const { stdin, lastFrame } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={state()}
|
|
|
|
|
|
chrome={chrome({ skipEmptyMessage: 'No selections. Skip or back?' })}
|
|
|
|
|
|
terminalRows={24}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={onExit}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('\r');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(normalizeFrameWrap(lastFrame())).toContain('No selections. Skip or back?');
|
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('n');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(lastFrame()).not.toContain('No selections. Skip or back?');
|
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('\r');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(lastFrame()).toContain('No selections. Skip or back?');
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('\r');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(onExit).toHaveBeenCalledWith({ kind: 'quit' });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('renders row-window overflow indicators when the visible list is clipped', async () => {
|
|
|
|
|
|
const onExit = vi.fn();
|
|
|
|
|
|
const initialState = buildInitialState({
|
|
|
|
|
|
tree: buildPickerTree(manyPages()),
|
|
|
|
|
|
existingSelectedIds: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
initialState.expanded = new Set([IDS.engineering]);
|
|
|
|
|
|
const { stdin, lastFrame } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={initialState}
|
|
|
|
|
|
chrome={chrome()}
|
|
|
|
|
|
terminalRows={13}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={onExit}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(lastFrame()).toContain('↓ 4 more');
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('[B');
|
|
|
|
|
|
stdin.write('[B');
|
|
|
|
|
|
stdin.write('[B');
|
|
|
|
|
|
stdin.write('[B');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
|
|
|
|
|
|
const frame = lastFrame() ?? '';
|
|
|
|
|
|
expect(frame).toContain('↑ ');
|
|
|
|
|
|
expect(frame).toContain('↓ ');
|
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('quits without saving on Ctrl+C', async () => {
|
|
|
|
|
|
const onExit = vi.fn();
|
|
|
|
|
|
const { stdin } = renderInkTest(
|
|
|
|
|
|
<TreePickerApp
|
|
|
|
|
|
initialState={state()}
|
|
|
|
|
|
chrome={chrome()}
|
|
|
|
|
|
terminalRows={24}
|
|
|
|
|
|
terminalWidth={100}
|
|
|
|
|
|
onExit={onExit}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('');
|
|
|
|
|
|
await waitForInkInput();
|
|
|
|
|
|
expect(onExit).toHaveBeenCalledWith({ kind: 'quit' });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('renderTreePickerTui', () => {
|
|
|
|
|
|
it('returns the app result from the Ink runtime', async () => {
|
|
|
|
|
|
const io = {
|
|
|
|
|
|
stdin: { isTTY: true, setRawMode: vi.fn() },
|
|
|
|
|
|
stdout: { isTTY: true, columns: 100, rows: 24, write: vi.fn() },
|
|
|
|
|
|
stderr: { write: vi.fn() },
|
|
|
|
|
|
};
|
|
|
|
|
|
const renderInk = vi.fn((_tree: ReactNode, _options: TreePickerInkRenderOptions) => fakeInkInstance());
|
|
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
|
renderTreePickerTui(
|
|
|
|
|
|
{ initialState: state(), chrome: chrome() },
|
|
|
|
|
|
io,
|
|
|
|
|
|
{ renderInk },
|
|
|
|
|
|
),
|
|
|
|
|
|
).resolves.toEqual({ kind: 'quit' });
|
|
|
|
|
|
expect(renderInk).toHaveBeenCalledOnce();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('sanitizes render errors and uses the supplied scripted-mode hint', async () => {
|
|
|
|
|
|
expect(sanitizeTreePickerTuiError(new Error('token=secret https://api.example.com/v1/search'))).toBe(
|
|
|
|
|
|
'[redacted] [redacted-url]',
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('falls back to quit with the scripted-mode hint when Ink cannot initialize', async () => {
|
|
|
|
|
|
let stderr = '';
|
|
|
|
|
|
const io = {
|
|
|
|
|
|
stdin: { isTTY: false, setRawMode: vi.fn() },
|
|
|
|
|
|
stdout: { isTTY: false, columns: 100, rows: 24, write: vi.fn() },
|
|
|
|
|
|
stderr: {
|
|
|
|
|
|
write(chunk: string) {
|
|
|
|
|
|
stderr += chunk;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
|
renderTreePickerTui(
|
|
|
|
|
|
{ initialState: state(), chrome: chrome() },
|
|
|
|
|
|
io,
|
|
|
|
|
|
{
|
|
|
|
|
|
renderInk: vi.fn(() => {
|
|
|
|
|
|
throw new Error('token=secret');
|
|
|
|
|
|
}),
|
|
|
|
|
|
scriptedModeHint: 'Use --no-input --foo bar for scripted mode.',
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
).resolves.toEqual({ kind: 'quit' });
|
|
|
|
|
|
expect(stderr).toContain('Use --no-input --foo bar for scripted mode.');
|
|
|
|
|
|
expect(stderr).not.toContain('secret');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|