Skip to content

Commit 48209ad

Browse files
authored
feat: Add blackbox TUI tests with snapshot verification (#45)
## Summary - Add comprehensive blackbox TUI tests with snapshot verification to ensure the terminal interface renders correctly across different input scenarios including initial state, user input, text wrapping, empty submission handling, unicode support, and multiline input
1 parent 32492b4 commit 48209ad

9 files changed

Lines changed: 324 additions & 0 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ tui-components = { path = "./tui-components" }
3535
[dev-dependencies]
3636
tempfile = "3"
3737
once_cell = "1"
38+
insta = "1.43"

tests/blackbox_tui_test.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2+
use insta::assert_snapshot;
3+
use nori_cli::app::{Message, Model};
4+
use nori_cli::ui;
5+
use ratatui::Terminal;
6+
use ratatui::backend::TestBackend;
7+
8+
/// Helper to create a KeyEvent for testing
9+
fn key_event(code: KeyCode) -> KeyEvent {
10+
KeyEvent::new(code, KeyModifiers::NONE)
11+
}
12+
13+
#[test]
14+
fn test_initial_app_renders_empty_textarea() {
15+
// Create a fresh app model
16+
let mut model = Model::default();
17+
18+
// Create a test terminal with standard dimensions
19+
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
20+
21+
// Render the app to the test terminal
22+
terminal
23+
.draw(|frame| ui::render(&mut model, frame))
24+
.unwrap();
25+
26+
// Snapshot the rendered output
27+
assert_snapshot!("initial_state", terminal.backend());
28+
}
29+
30+
#[test]
31+
fn test_typing_updates_textarea() {
32+
// Create a fresh app model
33+
let mut model = Model::default();
34+
35+
// Simulate typing "hi"
36+
model.update(Message::KeyPress(key_event(KeyCode::Char('h'))));
37+
model.update(Message::KeyPress(key_event(KeyCode::Char('i'))));
38+
39+
// Create terminal and render
40+
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
41+
terminal
42+
.draw(|frame| ui::render(&mut model, frame))
43+
.unwrap();
44+
45+
// Snapshot should show "hi" in the textarea
46+
assert_snapshot!("typed_hi", terminal.backend());
47+
}
48+
49+
#[test]
50+
fn test_long_input_wraps_properly() {
51+
let mut model = Model::default();
52+
53+
// Type a long message that will wrap in a 40-column terminal
54+
let long_text = "This is a very long message that should wrap properly in a narrow terminal";
55+
for ch in long_text.chars() {
56+
model.update(Message::KeyPress(key_event(KeyCode::Char(ch))));
57+
}
58+
59+
// Use narrow terminal to force wrapping
60+
let mut terminal = Terminal::new(TestBackend::new(40, 10)).unwrap();
61+
terminal
62+
.draw(|frame| ui::render(&mut model, frame))
63+
.unwrap();
64+
65+
assert_snapshot!("long_text_wrapping", terminal.backend());
66+
}
67+
68+
#[test]
69+
fn test_empty_input_does_not_submit() {
70+
let mut model = Model::default();
71+
72+
// Try to submit empty input (should not add to history)
73+
let events_before = model.response_events.len();
74+
model.update(Message::SubmitInput);
75+
let events_after = model.response_events.len();
76+
77+
// No new events should be added for empty submission
78+
assert_eq!(
79+
events_before, events_after,
80+
"Empty input should not create events"
81+
);
82+
83+
// UI should still show empty textarea
84+
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
85+
terminal
86+
.draw(|frame| ui::render(&mut model, frame))
87+
.unwrap();
88+
89+
assert_snapshot!("empty_submission_prevented", terminal.backend());
90+
}
91+
92+
#[test]
93+
fn test_unicode_input_renders_correctly() {
94+
let mut model = Model::default();
95+
96+
// Type unicode characters including emoji
97+
let unicode_text = "Hello 世界 👋";
98+
for ch in unicode_text.chars() {
99+
model.update(Message::KeyPress(key_event(KeyCode::Char(ch))));
100+
}
101+
102+
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
103+
terminal
104+
.draw(|frame| ui::render(&mut model, frame))
105+
.unwrap();
106+
107+
assert_snapshot!("unicode_input", terminal.backend());
108+
}
109+
110+
#[test]
111+
fn test_multiline_input() {
112+
let mut model = Model::default();
113+
114+
// Type text with newlines (Shift+Enter in the real UI, but we'll simulate with actual newlines)
115+
model.update(Message::KeyPress(key_event(KeyCode::Char('L'))));
116+
model.update(Message::KeyPress(key_event(KeyCode::Char('i'))));
117+
model.update(Message::KeyPress(key_event(KeyCode::Char('n'))));
118+
model.update(Message::KeyPress(key_event(KeyCode::Char('e'))));
119+
model.update(Message::KeyPress(key_event(KeyCode::Char(' '))));
120+
model.update(Message::KeyPress(key_event(KeyCode::Char('1'))));
121+
model.update(Message::KeyPress(KeyEvent::new(
122+
KeyCode::Enter,
123+
KeyModifiers::SHIFT,
124+
)));
125+
model.update(Message::KeyPress(key_event(KeyCode::Char('L'))));
126+
model.update(Message::KeyPress(key_event(KeyCode::Char('i'))));
127+
model.update(Message::KeyPress(key_event(KeyCode::Char('n'))));
128+
model.update(Message::KeyPress(key_event(KeyCode::Char('e'))));
129+
model.update(Message::KeyPress(key_event(KeyCode::Char(' '))));
130+
model.update(Message::KeyPress(key_event(KeyCode::Char('2'))));
131+
132+
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
133+
terminal
134+
.draw(|frame| ui::render(&mut model, frame))
135+
.unwrap();
136+
137+
assert_snapshot!("multiline_input", terminal.backend());
138+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: tests/blackbox_tui_test.rs
3+
expression: terminal.backend()
4+
---
5+
" "
6+
"› Write a message... "
7+
" "
8+
"Agent: No agent selected "
9+
" "
10+
"/switch-model: agents | /exit: quit "
11+
" "
12+
" "
13+
" "
14+
" "
15+
" "
16+
" "
17+
" "
18+
" "
19+
" "
20+
" "
21+
" "
22+
" "
23+
" "
24+
" "
25+
" "
26+
" "
27+
" "
28+
" "
29+
" "
30+
" "
31+
" "
32+
" "
33+
" "
34+
" "
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: tests/blackbox_tui_test.rs
3+
expression: terminal.backend()
4+
---
5+
" "
6+
"› Write a message... "
7+
" "
8+
"Agent: No agent selected "
9+
" "
10+
"/switch-model: agents | /exit: quit "
11+
" "
12+
" "
13+
" "
14+
" "
15+
" "
16+
" "
17+
" "
18+
" "
19+
" "
20+
" "
21+
" "
22+
" "
23+
" "
24+
" "
25+
" "
26+
" "
27+
" "
28+
" "
29+
" "
30+
" "
31+
" "
32+
" "
33+
" "
34+
" "
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
source: tests/blackbox_tui_test.rs
3+
expression: terminal.backend()
4+
---
5+
" "
6+
" This is a very long message that "
7+
"› should wrap properly in a narrow "
8+
" terminal "
9+
" "
10+
"Agent: No agent selected "
11+
" "
12+
"/switch-model: agents | /exit: quit "
13+
" "
14+
" "
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: tests/blackbox_tui_test.rs
3+
expression: terminal.backend()
4+
---
5+
" "
6+
" Line 1 "
7+
"› Line 2 "
8+
" "
9+
"Agent: No agent selected "
10+
" "
11+
"/switch-model: agents | /exit: quit "
12+
" "
13+
" "
14+
" "
15+
" "
16+
" "
17+
" "
18+
" "
19+
" "
20+
" "
21+
" "
22+
" "
23+
" "
24+
" "
25+
" "
26+
" "
27+
" "
28+
" "
29+
" "
30+
" "
31+
" "
32+
" "
33+
" "
34+
" "
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: tests/blackbox_tui_test.rs
3+
expression: terminal.backend()
4+
---
5+
" "
6+
"› hi "
7+
" "
8+
"Agent: No agent selected "
9+
" "
10+
"/switch-model: agents | /exit: quit "
11+
" "
12+
" "
13+
" "
14+
" "
15+
" "
16+
" "
17+
" "
18+
" "
19+
" "
20+
" "
21+
" "
22+
" "
23+
" "
24+
" "
25+
" "
26+
" "
27+
" "
28+
" "
29+
" "
30+
" "
31+
" "
32+
" "
33+
" "
34+
" "

0 commit comments

Comments
 (0)