Skip to content

Commit 654aa66

Browse files
committed
test: add TUI component, guard, and hook tests with keyboard simulation
1 parent f6c4732 commit 654aa66

File tree

10 files changed

+1248
-144
lines changed

10 files changed

+1248
-144
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { DeployMessage } from '../../../cdk/toolkit-lib/index.js';
2+
import { DeployStatus } from '../DeployStatus.js';
3+
import { render } from 'ink-testing-library';
4+
import React from 'react';
5+
import { describe, expect, it } from 'vitest';
6+
7+
function makeMsg(
8+
message: string,
9+
code = 'CDK_TOOLKIT_I5502',
10+
progress?: { completed: number; total: number }
11+
): DeployMessage {
12+
return { message, code, level: 'info', time: new Date(), timestamp: new Date(), progress } as DeployMessage;
13+
}
14+
15+
describe('DeployStatus', () => {
16+
it('renders deploying state with gradient text', () => {
17+
const { lastFrame } = render(<DeployStatus messages={[]} isComplete={false} hasError={false} />);
18+
19+
expect(lastFrame()).toContain('Deploying to AWS');
20+
});
21+
22+
it('renders success state when complete', () => {
23+
const { lastFrame } = render(<DeployStatus messages={[]} isComplete={true} hasError={false} />);
24+
25+
expect(lastFrame()).toContain('Deploy to AWS Complete');
26+
});
27+
28+
it('renders failure state when complete with error', () => {
29+
const { lastFrame } = render(<DeployStatus messages={[]} isComplete={true} hasError={true} />);
30+
31+
expect(lastFrame()).toContain('Deploy to AWS Failed');
32+
});
33+
34+
it('renders resource events during deployment', () => {
35+
const messages = [
36+
makeMsg('MyStack | CREATE_IN_PROGRESS | AWS::Lambda::Function | MyFunc'),
37+
makeMsg('MyStack | CREATE_COMPLETE | AWS::Lambda::Function | MyFunc'),
38+
];
39+
40+
const { lastFrame } = render(<DeployStatus messages={messages} isComplete={false} hasError={false} />);
41+
42+
expect(lastFrame()).toContain('Lambda::Function');
43+
expect(lastFrame()).toContain('CREATE_COMPLETE');
44+
});
45+
46+
it('renders progress bar when progress data exists', () => {
47+
const messages = [makeMsg('deploying', 'CDK_TOOLKIT_I5502', { completed: 3, total: 10 })];
48+
49+
const { lastFrame } = render(<DeployStatus messages={messages} isComplete={false} hasError={false} />);
50+
51+
expect(lastFrame()).toContain('3/10');
52+
});
53+
54+
it('skips CLEANUP messages', () => {
55+
const messages = [
56+
makeMsg('MyStack | CREATE_COMPLETE | AWS::Lambda::Function | MyFunc'),
57+
makeMsg('MyStack | CLEANUP_IN_PROGRESS | AWS::Lambda::Function | OldFunc'),
58+
];
59+
60+
const { lastFrame } = render(<DeployStatus messages={messages} isComplete={false} hasError={false} />);
61+
62+
expect(lastFrame()).toContain('Lambda::Function');
63+
expect(lastFrame()).toContain('CREATE_COMPLETE');
64+
});
65+
66+
it('ignores non-resource-event messages', () => {
67+
const messages = [makeMsg('Some general info', 'CDK_TOOLKIT_I1234')];
68+
69+
const { lastFrame } = render(<DeployStatus messages={messages} isComplete={false} hasError={false} />);
70+
71+
// Should still show the deploying text but no resource lines
72+
expect(lastFrame()).toContain('Deploying to AWS');
73+
});
74+
});
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { ConfirmPrompt, ErrorPrompt, PromptScreen, SuccessPrompt } from '../PromptScreen.js';
2+
import { Text } from 'ink';
3+
import { render } from 'ink-testing-library';
4+
import React from 'react';
5+
import { afterEach, describe, expect, it, vi } from 'vitest';
6+
7+
const ENTER = '\r';
8+
const ESCAPE = '\x1B';
9+
10+
afterEach(() => {
11+
vi.restoreAllMocks();
12+
});
13+
14+
describe('PromptScreen', () => {
15+
it('renders children and help text', () => {
16+
const { lastFrame } = render(
17+
<PromptScreen helpText="Press Enter">
18+
<Text>Hello</Text>
19+
</PromptScreen>
20+
);
21+
22+
expect(lastFrame()).toContain('Hello');
23+
expect(lastFrame()).toContain('Press Enter');
24+
});
25+
26+
it('calls onConfirm on Enter key', async () => {
27+
const onConfirm = vi.fn();
28+
const { stdin } = render(
29+
<PromptScreen helpText="help" onConfirm={onConfirm}>
30+
<Text>msg</Text>
31+
</PromptScreen>
32+
);
33+
34+
await new Promise(resolve => setTimeout(resolve, 50));
35+
stdin.write(ENTER);
36+
await new Promise(resolve => setTimeout(resolve, 50));
37+
38+
expect(onConfirm).toHaveBeenCalledTimes(1);
39+
});
40+
41+
it('calls onConfirm on y key', async () => {
42+
const onConfirm = vi.fn();
43+
const { stdin } = render(
44+
<PromptScreen helpText="help" onConfirm={onConfirm}>
45+
<Text>msg</Text>
46+
</PromptScreen>
47+
);
48+
49+
await new Promise(resolve => setTimeout(resolve, 50));
50+
stdin.write('y');
51+
await new Promise(resolve => setTimeout(resolve, 50));
52+
53+
expect(onConfirm).toHaveBeenCalledTimes(1);
54+
});
55+
56+
it('calls onExit on Escape key', async () => {
57+
const onExit = vi.fn();
58+
const { stdin } = render(
59+
<PromptScreen helpText="help" onExit={onExit}>
60+
<Text>msg</Text>
61+
</PromptScreen>
62+
);
63+
64+
await new Promise(resolve => setTimeout(resolve, 50));
65+
stdin.write(ESCAPE);
66+
await new Promise(resolve => setTimeout(resolve, 50));
67+
68+
expect(onExit).toHaveBeenCalledTimes(1);
69+
});
70+
71+
it('calls onExit on n key', async () => {
72+
const onExit = vi.fn();
73+
const { stdin } = render(
74+
<PromptScreen helpText="help" onExit={onExit}>
75+
<Text>msg</Text>
76+
</PromptScreen>
77+
);
78+
79+
await new Promise(resolve => setTimeout(resolve, 50));
80+
stdin.write('n');
81+
await new Promise(resolve => setTimeout(resolve, 50));
82+
83+
expect(onExit).toHaveBeenCalledTimes(1);
84+
});
85+
86+
it('calls onBack on b key', async () => {
87+
const onBack = vi.fn();
88+
const { stdin } = render(
89+
<PromptScreen helpText="help" onBack={onBack}>
90+
<Text>msg</Text>
91+
</PromptScreen>
92+
);
93+
94+
await new Promise(resolve => setTimeout(resolve, 50));
95+
stdin.write('b');
96+
await new Promise(resolve => setTimeout(resolve, 50));
97+
98+
expect(onBack).toHaveBeenCalledTimes(1);
99+
});
100+
101+
it('ignores input when inputEnabled is false', async () => {
102+
const onConfirm = vi.fn();
103+
const onExit = vi.fn();
104+
const { stdin } = render(
105+
<PromptScreen helpText="help" onConfirm={onConfirm} onExit={onExit} inputEnabled={false}>
106+
<Text>msg</Text>
107+
</PromptScreen>
108+
);
109+
110+
await new Promise(resolve => setTimeout(resolve, 50));
111+
stdin.write(ENTER);
112+
stdin.write(ESCAPE);
113+
await new Promise(resolve => setTimeout(resolve, 50));
114+
115+
expect(onConfirm).not.toHaveBeenCalled();
116+
expect(onExit).not.toHaveBeenCalled();
117+
});
118+
});
119+
120+
describe('SuccessPrompt', () => {
121+
it('renders success message', () => {
122+
const { lastFrame } = render(<SuccessPrompt message="Deployment complete" />);
123+
124+
expect(lastFrame()).toContain('Deployment complete');
125+
});
126+
127+
it('renders detail text when provided', () => {
128+
const { lastFrame } = render(<SuccessPrompt message="Done" detail="3 agents deployed" />);
129+
130+
expect(lastFrame()).toContain('3 agents deployed');
131+
});
132+
133+
it('shows confirm and exit help text when onConfirm provided', () => {
134+
const { lastFrame } = render(<SuccessPrompt message="Done" onConfirm={vi.fn()} onExit={vi.fn()} />);
135+
136+
expect(lastFrame()).toContain('continue');
137+
expect(lastFrame()).toContain('exit');
138+
});
139+
140+
it('shows any key help text when no onConfirm', () => {
141+
const { lastFrame } = render(<SuccessPrompt message="Done" onExit={vi.fn()} />);
142+
143+
expect(lastFrame()).toContain('any key');
144+
});
145+
146+
it('uses custom confirmText and exitText', () => {
147+
const { lastFrame } = render(
148+
<SuccessPrompt message="Done" onConfirm={vi.fn()} confirmText="Deploy" exitText="Cancel" />
149+
);
150+
151+
expect(lastFrame()).toContain('deploy');
152+
expect(lastFrame()).toContain('cancel');
153+
});
154+
});
155+
156+
describe('ErrorPrompt', () => {
157+
it('renders error message with cross mark', () => {
158+
const { lastFrame } = render(<ErrorPrompt message="Something failed" />);
159+
160+
expect(lastFrame()).toContain('✗');
161+
expect(lastFrame()).toContain('Something failed');
162+
});
163+
164+
it('renders detail text when provided', () => {
165+
const { lastFrame } = render(<ErrorPrompt message="Failed" detail="Stack rollback" />);
166+
167+
expect(lastFrame()).toContain('Stack rollback');
168+
});
169+
170+
it('shows back and exit help text', () => {
171+
const { lastFrame } = render(<ErrorPrompt message="Failed" onBack={vi.fn()} onExit={vi.fn()} />);
172+
173+
expect(lastFrame()).toContain('Enter/B to go back');
174+
expect(lastFrame()).toContain('Esc/Q to exit');
175+
});
176+
177+
it('calls onBack on Enter key', async () => {
178+
const onBack = vi.fn();
179+
const { stdin } = render(<ErrorPrompt message="Failed" onBack={onBack} />);
180+
181+
await new Promise(resolve => setTimeout(resolve, 50));
182+
stdin.write(ENTER);
183+
await new Promise(resolve => setTimeout(resolve, 50));
184+
185+
expect(onBack).toHaveBeenCalledTimes(1);
186+
});
187+
188+
it('calls onBack on b key', async () => {
189+
const onBack = vi.fn();
190+
const { stdin } = render(<ErrorPrompt message="Failed" onBack={onBack} />);
191+
192+
await new Promise(resolve => setTimeout(resolve, 50));
193+
stdin.write('b');
194+
await new Promise(resolve => setTimeout(resolve, 50));
195+
196+
expect(onBack).toHaveBeenCalledTimes(1);
197+
});
198+
199+
it('calls onExit on Escape key', async () => {
200+
const onExit = vi.fn();
201+
const { stdin } = render(<ErrorPrompt message="Failed" onExit={onExit} />);
202+
203+
await new Promise(resolve => setTimeout(resolve, 50));
204+
stdin.write(ESCAPE);
205+
await new Promise(resolve => setTimeout(resolve, 50));
206+
207+
expect(onExit).toHaveBeenCalledTimes(1);
208+
});
209+
210+
it('calls onExit on n key', async () => {
211+
const onExit = vi.fn();
212+
const { stdin } = render(<ErrorPrompt message="Failed" onExit={onExit} />);
213+
214+
await new Promise(resolve => setTimeout(resolve, 50));
215+
stdin.write('n');
216+
await new Promise(resolve => setTimeout(resolve, 50));
217+
218+
expect(onExit).toHaveBeenCalledTimes(1);
219+
});
220+
});
221+
222+
describe('ConfirmPrompt', () => {
223+
it('renders confirmation message', () => {
224+
const { lastFrame } = render(<ConfirmPrompt message="Delete agent?" onConfirm={vi.fn()} onCancel={vi.fn()} />);
225+
226+
expect(lastFrame()).toContain('Delete agent?');
227+
});
228+
229+
it('renders detail when provided', () => {
230+
const { lastFrame } = render(
231+
<ConfirmPrompt message="Delete?" detail="This is irreversible" onConfirm={vi.fn()} onCancel={vi.fn()} />
232+
);
233+
234+
expect(lastFrame()).toContain('This is irreversible');
235+
});
236+
237+
it('shows keyboard help when showInput is false', () => {
238+
const { lastFrame } = render(<ConfirmPrompt message="Delete?" onConfirm={vi.fn()} onCancel={vi.fn()} />);
239+
240+
expect(lastFrame()).toContain('Enter/Y confirm');
241+
expect(lastFrame()).toContain('Esc/N cancel');
242+
});
243+
244+
it('shows input help when showInput is true', () => {
245+
const { lastFrame } = render(<ConfirmPrompt message="Delete?" showInput onConfirm={vi.fn()} onCancel={vi.fn()} />);
246+
247+
expect(lastFrame()).toContain('Type y/n');
248+
});
249+
250+
it('calls onConfirm on Enter key (no showInput)', async () => {
251+
const onConfirm = vi.fn();
252+
const { stdin } = render(<ConfirmPrompt message="Delete?" onConfirm={onConfirm} onCancel={vi.fn()} />);
253+
254+
await new Promise(resolve => setTimeout(resolve, 50));
255+
stdin.write(ENTER);
256+
await new Promise(resolve => setTimeout(resolve, 50));
257+
258+
expect(onConfirm).toHaveBeenCalledTimes(1);
259+
});
260+
261+
it('calls onCancel on Escape key', async () => {
262+
const onCancel = vi.fn();
263+
const { stdin } = render(<ConfirmPrompt message="Delete?" onConfirm={vi.fn()} onCancel={onCancel} />);
264+
265+
await new Promise(resolve => setTimeout(resolve, 50));
266+
stdin.write(ESCAPE);
267+
await new Promise(resolve => setTimeout(resolve, 50));
268+
269+
expect(onCancel).toHaveBeenCalledTimes(1);
270+
});
271+
});

0 commit comments

Comments
 (0)