Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-security": "^3.0.1",
"husky": "^9.1.7",
"ink-testing-library": "^4.0.0",
"lint-staged": "^16.2.7",
"prettier": "^3.7.4",
"secretlint": "^11.3.0",
Expand Down
30 changes: 30 additions & 0 deletions src/cli/tui/__tests__/exit-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { clearExitMessage, getExitMessage, setExitMessage } from '../exit-message.js';
import { afterEach, describe, expect, it } from 'vitest';

describe('exit-message', () => {
afterEach(() => clearExitMessage());

it('returns null when no message set', () => {
expect(getExitMessage()).toBeNull();
});

it('stores and retrieves a message', () => {
setExitMessage('Goodbye!');

expect(getExitMessage()).toBe('Goodbye!');
});

it('clears the message', () => {
setExitMessage('Bye');
clearExitMessage();

expect(getExitMessage()).toBeNull();
});

it('overwrites previous message', () => {
setExitMessage('First');
setExitMessage('Second');

expect(getExitMessage()).toBe('Second');
});
});
35 changes: 35 additions & 0 deletions src/cli/tui/components/__tests__/AwsTargetConfigUI.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getAwsConfigHelpText } from '../AwsTargetConfigUI.js';
import { describe, expect, it } from 'vitest';

describe('getAwsConfigHelpText', () => {
it('returns exact help text for choice phase', () => {
expect(getAwsConfigHelpText('choice')).toBe('↑↓ navigate · Enter select · Esc exit');
});

it('returns same help text for token-expired as choice', () => {
expect(getAwsConfigHelpText('token-expired')).toBe(getAwsConfigHelpText('choice'));
});

it('returns exact help text for select-target phase', () => {
expect(getAwsConfigHelpText('select-target')).toBe('↑↓ navigate · Space toggle · Enter deploy · Esc exit');
});

it('returns exact help text for manual-account phase', () => {
expect(getAwsConfigHelpText('manual-account')).toBe('12-digit account ID · Esc back');
});

it('returns exact help text for manual-region phase', () => {
expect(getAwsConfigHelpText('manual-region')).toBe('Type to filter · ↑↓ navigate · Enter select · Esc back');
});

it('returns undefined for loading phases', () => {
expect(getAwsConfigHelpText('checking')).toBeUndefined();
expect(getAwsConfigHelpText('detecting')).toBeUndefined();
expect(getAwsConfigHelpText('saving')).toBeUndefined();
});

it('returns undefined for terminal phases', () => {
expect(getAwsConfigHelpText('configured')).toBeUndefined();
expect(getAwsConfigHelpText('error')).toBeUndefined();
});
});
89 changes: 89 additions & 0 deletions src/cli/tui/components/__tests__/ConfirmReview.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ConfirmReview } from '../ConfirmReview.js';
import { render } from 'ink-testing-library';
import React from 'react';
import { describe, expect, it } from 'vitest';

describe('ConfirmReview', () => {
it('renders default title and help text', () => {
const { lastFrame } = render(<ConfirmReview fields={[{ label: 'Name', value: 'my-agent' }]} />);
const frame = lastFrame()!;

expect(frame).toContain('Review Configuration');
expect(frame).toContain('Enter confirm');
expect(frame).toContain('Esc back');
});

it('renders custom title', () => {
const { lastFrame } = render(
<ConfirmReview title="Review Deploy" fields={[{ label: 'Target', value: 'us-east-1' }]} />
);

expect(lastFrame()).toContain('Review Deploy');
expect(lastFrame()).not.toContain('Review Configuration');
});

it('renders each field as label: value on the same line', () => {
const { lastFrame } = render(
<ConfirmReview
fields={[
{ label: 'Name', value: 'my-agent' },
{ label: 'SDK', value: 'Strands' },
{ label: 'Language', value: 'Python' },
]}
/>
);
const lines = lastFrame()!.split('\n');

// Each label and its value should appear on the same line
const nameLine = lines.find(l => l.includes('Name'))!;
expect(nameLine).toContain('my-agent');

const sdkLine = lines.find(l => l.includes('SDK'))!;
expect(sdkLine).toContain('Strands');

const langLine = lines.find(l => l.includes('Language'))!;
expect(langLine).toContain('Python');
});

it('renders label with colon separator', () => {
const { lastFrame } = render(<ConfirmReview fields={[{ label: 'Region', value: 'us-east-1' }]} />);
const lines = lastFrame()!.split('\n');

const regionLine = lines.find(l => l.includes('Region'))!;
expect(regionLine).toMatch(/Region.*:.*us-east-1/);
});

it('renders custom help text replacing default', () => {
const { lastFrame } = render(
<ConfirmReview fields={[{ label: 'Name', value: 'test' }]} helpText="Press Y to confirm" />
);

expect(lastFrame()).toContain('Press Y to confirm');
expect(lastFrame()).not.toContain('Enter confirm');
});

it('renders multiple fields in order', () => {
const { lastFrame } = render(
<ConfirmReview
fields={[
{ label: 'First', value: 'A' },
{ label: 'Second', value: 'B' },
{ label: 'Third', value: 'C' },
]}
/>
);
const frame = lastFrame()!;

// All three labels should be present
expect(frame).toContain('First');
expect(frame).toContain('Second');
expect(frame).toContain('Third');

// Verify ordering: First appears before Second
const firstIdx = frame.indexOf('First');
const secondIdx = frame.indexOf('Second');
const thirdIdx = frame.indexOf('Third');
expect(firstIdx).toBeLessThan(secondIdx);
expect(secondIdx).toBeLessThan(thirdIdx);
});
});
83 changes: 83 additions & 0 deletions src/cli/tui/components/__tests__/CredentialSourcePrompt.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { CredentialSourcePrompt } from '../CredentialSourcePrompt.js';
import { render } from 'ink-testing-library';
import React from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';

const ENTER = '\r';

afterEach(() => vi.restoreAllMocks());

const defaultProps = {
missingCredentials: [
{ providerName: 'OpenAI', envVarName: 'OPENAI_API_KEY' },
{ providerName: 'Anthropic', envVarName: 'ANTHROPIC_API_KEY' },
],
onUseEnvLocal: vi.fn(),
onManualEntry: vi.fn(),
onSkip: vi.fn(),
};

describe('CredentialSourcePrompt', () => {
it('renders title', () => {
const { lastFrame } = render(<CredentialSourcePrompt {...defaultProps} />);

expect(lastFrame()).toContain('Identity Provider Setup');
});

it('renders provider names', () => {
const { lastFrame } = render(<CredentialSourcePrompt {...defaultProps} />);
const frame = lastFrame()!;

expect(frame).toContain('OpenAI');
expect(frame).toContain('Anthropic');
});

it('renders credential count', () => {
const { lastFrame } = render(<CredentialSourcePrompt {...defaultProps} />);

expect(lastFrame()).toContain('2 identity providers');
});

it('renders singular provider count', () => {
const { lastFrame } = render(
<CredentialSourcePrompt
{...defaultProps}
missingCredentials={[{ providerName: 'OpenAI', envVarName: 'OPENAI_API_KEY' }]}
/>
);

expect(lastFrame()).toContain('1 identity provider configured');
});

it('renders source options', () => {
const { lastFrame } = render(<CredentialSourcePrompt {...defaultProps} />);
const frame = lastFrame()!;

expect(frame).toContain('.env.local');
expect(frame).toContain('Enter credentials manually');
expect(frame).toContain('Skip for now');
});

it('calls onUseEnvLocal when first option selected', () => {
const onUseEnvLocal = vi.fn();
const { stdin } = render(<CredentialSourcePrompt {...defaultProps} onUseEnvLocal={onUseEnvLocal} />);

// First option is already selected
stdin.write(ENTER);

expect(onUseEnvLocal).toHaveBeenCalledTimes(1);
});

it('renders "Not saved to disk" description for manual entry option', () => {
const { lastFrame } = render(<CredentialSourcePrompt {...defaultProps} />);

expect(lastFrame()).toContain('Not saved to disk');
});

it('shows navigation help text', () => {
const { lastFrame } = render(<CredentialSourcePrompt {...defaultProps} />);

expect(lastFrame()).toContain('navigate');
expect(lastFrame()).toContain('Enter select');
});
});
40 changes: 40 additions & 0 deletions src/cli/tui/components/__tests__/Cursor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Cursor } from '../Cursor.js';
import { render } from 'ink-testing-library';
import React from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';

afterEach(() => vi.restoreAllMocks());

describe('Cursor', () => {
it('renders the provided character on initial mount', () => {
const { lastFrame } = render(<Cursor char="X" />);
expect(lastFrame()).toContain('X');
});

it('sets up a blink interval using setInterval', () => {
const spy = vi.spyOn(globalThis, 'setInterval');
render(<Cursor char="A" interval={500} />);
// Cursor uses setInterval with the provided interval for blinking
expect(spy).toHaveBeenCalledWith(expect.any(Function), 500);
});

it('uses custom interval value for the blink timer', () => {
const spy = vi.spyOn(globalThis, 'setInterval');
render(<Cursor char="B" interval={200} />);
expect(spy).toHaveBeenCalledWith(expect.any(Function), 200);
});

it('renders with default space character when no char prop given', () => {
const { lastFrame } = render(<Cursor />);
// Default char is a space — component should render without errors
expect(lastFrame()).toBeDefined();
});

it('cleans up interval timer on unmount', () => {
const spy = vi.spyOn(globalThis, 'clearInterval');
const { unmount } = render(<Cursor char="C" interval={200} />);
unmount();
// clearInterval should be called during cleanup
expect(spy).toHaveBeenCalled();
});
});
Loading
Loading