返回 CodeWhale
mounted_composer_tests.rs
根目录 / crates / tui / src / tui / widgets / mounted_composer_tests.rs
1 //! Acceptance of the adopted mounted composer against its frozen painter.
2 use super::*;
3 use crate::config::Config;
4 use std::path::PathBuf;
5
6 fn app() -> App {
7 let mut app = App::new(
8 crate::test_support::test_tui_options(PathBuf::from(".")),
9 &Config::default(),
10 );
11 app.launch.visible = false;
12 app.composer.vim_enabled = false;
13 app.ui_locale = Locale::En;
14 app.theme_id = palette::ThemeId::Underwater;
15 app.ui_theme = palette::UNDERWATER_UI_THEME;
16 app
17 }
18 #[test]
19 fn mounted_kit_exact_buffer_caret_height_and_menu_facts_match_legacy() {
20 for &locale in Locale::shipped() {
21 for case in 0..8 {
22 let mut app = app();
23 app.ui_locale = locale;
24 app.input = match case {
25 0 => "",
26 1 => "hello",
27 2 => "a CJK 鲸鱼 draft with cafe\u{0301} and a long https://example.com/path",
28 3 => "1\u{fe0f}\u{20e3} 👩\u{200d}💻\n\nlast row",
29 4 => "select these words across rows",
30 5 => "/test",
31 6 => "@builder",
32 _ => "",
33 }
34 .into();
35 app.cursor_position = app.input.chars().count();
36 if case == 4 {
37 app.selection_anchor = Some(2);
38 }
39 if case == 7 {
40 app.start_history_search();
41 }
42 let slash = if case == 5 {
43 vec![
44 SlashMenuEntry {
45 name: "/test".into(),
46 description: "Run checks for this project".into(),
47 is_skill: false,
48 alias_hint: Some("jiancha".into()),
49 },
50 SlashMenuEntry {
51 name: "/review".into(),
52 description: "Review every changed source file".into(),
53 is_skill: true,
54 alias_hint: None,
55 },
56 ]
57 } else {
58 Vec::new()
59 };
60 let mentions = if case == 6 {
61 vec!["builder".into(), "reviewer 鲸鱼".into()]
62 } else {
63 Vec::new()
64 };
65 for density in [
66 ComposerDensity::Compact,
67 ComposerDensity::Comfortable,
68 ComposerDensity::Spacious,
69 ] {
70 app.composer_density = density;
71 for enclosed in [false, true] {
72 app.composer_border = enclosed;
73 for width in [20, 40, 80, 120] {
74 for height in [3, 5, 8, 12] {
75 let area = Rect::new(7, 5, width, height);
76 let canvas = Rect::new(2, 3, width + 12, height + 8);
77 let mut actual = Buffer::empty(canvas);
78 for cell in &mut actual.content {
79 cell.set_symbol("~")
80 .set_style(Style::default().bg(Color::Rgb(11, 23, 37)));
81 }
82 let mut expected = actual.clone();
83 let legacy =
84 legacy_composer::ComposerWidget::new(&app, 12, &slash, &mentions);
85 let widget = ComposerWidget::new(&app, 12, &slash, &mentions);
86 assert_eq!(widget.desired_height(width), legacy.desired_height(width));
87 assert_eq!(widget.cursor_pos(area), legacy.cursor_pos(area));
88 assert_eq!(
89 active_composer_submit_rect(&app, area),
90 legacy_composer::active_composer_submit_rect(&app, area)
91 );
92 assert_eq!(
93 wrap_input_lines_for_mouse(&app.input, usize::from(width)),
94 legacy_composer::wrap_input_lines_for_mouse(
95 &app.input,
96 usize::from(width)
97 )
98 );
99 legacy.render(area, &mut expected);
100 let old_boxes = app.viewport.last_slash_menu_hitboxes.borrow().clone();
101 widget.render(area, &mut actual);
102 assert_eq!(
103 actual, expected,
104 "locale={locale:?} case={case} density={density:?} enclosed={enclosed} area={area:?}"
105 );
106 let plan = widget.plan(area);
107 let boxes = app.viewport.last_slash_menu_hitboxes.borrow().clone();
108 assert_eq!(boxes, plan.menu_rects);
109 // Full wrapped option rows intentionally replace old one-row targets.
110 if boxes.iter().all(|(_, rect)| rect.height == 1) {
111 assert_eq!(boxes, old_boxes);
112 }
113 for (_, rect) in boxes {
114 assert_eq!(rect.intersection(plan.geometry.inner), rect);
115 }
116 }
117 }
118 }
119 }
120 }
121 }
122 }
123 #[test]
124 fn mounted_kit_live_custom_theme_slots_remain_exact() {
125 let mut app = app();
126 app.input = "select words".into();
127 app.cursor_position = 6;
128 app.selection_anchor = Some(0);
129 app.composer_border = true;
130 app.ui_theme.composer_bg = Color::Rgb(9, 21, 33);
131 app.ui_theme.accent_primary = Color::Rgb(41, 52, 63);
132 app.ui_theme.selection_bg = Color::Rgb(71, 82, 93);
133 app.ui_theme.info = Color::Rgb(101, 112, 123);
134 let widget = ComposerWidget::new(&app, 9, &[], &[]);
135 let area = Rect::new(7, 5, 80, 7);
136 let mut buf = Buffer::empty(area);
137 widget.render(area, &mut buf);
138 let plan = widget.plan(area);
139 assert_eq!(buf[(area.x, area.y)].fg, app.ui_theme.accent_primary);
140 assert_eq!(
141 buf[(plan.geometry.text.x, plan.cursor.unwrap().y)].bg,
142 app.ui_theme.selection_bg
143 );
144 let submit = plan.geometry.submit.unwrap();
145 assert_eq!(
146 buf[(submit.x, submit.y)].fg,
147 palette::chrome_style(&app.ui_theme, palette::ChromeInk::Info)
148 .fg
149 .unwrap()
150 );
151 for y in area.y..area.bottom() {
152 for x in area.x..area.right() {
153 assert_eq!(
154 buf[(x, y)].bg,
155 if x >= plan.geometry.text.x
156 && x < plan.cursor.unwrap().x
157 && y == plan.cursor.unwrap().y
158 {
159 app.ui_theme.selection_bg
160 } else {
161 app.ui_theme.composer_bg
162 }
163 );
164 }
165 }
166 }
167 #[test]
168 fn mounted_kit_shared_centered_padding_drives_source_pointer_projection() {
169 let mut app = app();
170 app.composer_border = true;
171 app.input = "hello".into();
172 app.cursor_position = 2;
173 let widget = ComposerWidget::new(&app, 9, &[], &[]);
174 let plan = widget.plan(Rect::new(7, 5, 40, 7));
175 assert_eq!(plan.top_padding, 2);
176 let cursor = plan.cursor.unwrap();
177 let index = codewhale_ratatui::native_composer_source_at(
178 &app.input,
179 usize::from(plan.geometry.text.width),
180 usize::from(cursor.x - plan.geometry.text.x),
181 usize::from(cursor.y - plan.geometry.text.y),
182 plan.scroll_offset,
183 plan.top_padding,
184 );
185 assert_eq!(
186 index, app.cursor_position,
187 "viewport must retain the painter's centered padding, not bottom padding"
188 );
189 app.input = "a\t\u{202e}中b".into();
190 app.cursor_position = 4;
191 let plan = ComposerWidget::new(&app, 9, &[], &[]).plan(Rect::new(7, 5, 40, 7));
192 let cursor = plan.cursor.unwrap();
193 assert_eq!(
194 codewhale_ratatui::native_composer_source_at(
195 &app.input,
196 usize::from(plan.geometry.text.width),
197 usize::from(cursor.x - plan.geometry.text.x),
198 usize::from(cursor.y - plan.geometry.text.y),
199 plan.scroll_offset,
200 plan.top_padding
201 ),
202 4
203 );
204 assert_eq!(app.input, "a\t\u{202e}中b");
205 }
206 #[test]
207 fn mounted_kit_resize_to_empty_withdraws_slash_targets() {
208 let mut app = app();
209 app.input = "/test".into();
210 app.cursor_position = 5;
211 let slash = vec![SlashMenuEntry {
212 name: "/test".into(),
213 description: "test project".into(),
214 is_skill: false,
215 alias_hint: None,
216 }];
217 let widget = ComposerWidget::new(&app, 9, &slash, &[]);
218 let area = Rect::new(7, 5, 80, 9);
219 let mut buf = Buffer::empty(area);
220 widget.render(area, &mut buf);
221 assert!(!app.viewport.last_slash_menu_hitboxes.borrow().is_empty());
222 for zero in [Rect::new(7, 5, 0, 9), Rect::new(7, 5, 80, 0)] {
223 widget.render(zero, &mut buf);
224 assert!(app.viewport.last_slash_menu_hitboxes.borrow().is_empty());
225 assert!(widget.cursor_pos(zero).is_none());
226 assert!(active_composer_submit_rect(&app, zero).is_none());
227 }
228 }
229
229 lines RUST