返回 CodeWhale
mounted_transcript_tests.rs
根目录 / crates / tui / src / tui / widgets / mounted_transcript_tests.rs
1 //! Whole mounted transcript acceptance against private frozen production code.
2 use super::*;
3 use crate::config::Config;
4 use crate::tui::selection::TranscriptSelectionPoint;
5 use std::path::PathBuf;
6
7 fn app(locale: Locale, case: usize) -> App {
8 let mut app = App::new(
9 crate::test_support::test_tui_options(PathBuf::from(".")),
10 &Config::default(),
11 );
12 app.launch.visible = false;
13 app.ui_locale = locale;
14 app.theme_id = palette::ThemeId::Terminal;
15 app.ui_theme = palette::TERMINAL_UI_THEME;
16 app.low_motion = true;
17 app.fancy_animations = false;
18 if case == 0 {
19 return app;
20 }
21 app.push_history_cell(HistoryCell::User {
22 content: "Review this source and its exact source copy".into(),
23 });
24 app.push_history_cell(HistoryCell::Assistant {
25 content: "A linked [guide](https://example.test/private-target) with 鲸鱼 cafe\u{0301} 1\u{20e3}① and 👩\u{200d}💻.\n\n```rust\nlet source = exact_source;\n```".into(), streaming: case == 2,
26 });
27 if case >= 3 {
28 app.use_mouse_capture = true;
29 for n in 0..8 {
30 app.push_history_cell(HistoryCell::User {
31 content: format!("User turn {n}"),
32 });
33 app.push_history_cell(HistoryCell::Assistant {
34 content: format!("Receipt {n} with enough text to wrap in a narrow transcript."),
35 streaming: false,
36 });
37 }
38 app.viewport.transcript_scroll =
39 crate::tui::scrolling::TranscriptScroll::at_line(if case == 4 { 3 } else { 0 });
40 }
41 if case == 4 {
42 app.viewport.transcript_selection.anchor = Some(TranscriptSelectionPoint {
43 line_index: 3,
44 column: 1,
45 });
46 app.viewport.transcript_selection.head = Some(TranscriptSelectionPoint {
47 line_index: 5,
48 column: 9,
49 });
50 }
51 if case == 5 {
52 // A user theme keeps independent source colors across native chrome.
53 app.ui_theme.surface_bg = Color::Rgb(13, 29, 47);
54 app.ui_theme.border = Color::Rgb(61, 79, 101);
55 app.ui_theme.status_working = Color::Rgb(127, 149, 173);
56 app.ui_theme.selection_bg = Color::Rgb(181, 197, 211);
57 app.push_history_cell(HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
58 name: "read_file".into(),
59 status: ToolStatus::Success,
60 input_summary: Some("src/main.rs".into()),
61 output: Some("kept source and warning details".into()),
62 prompts: None,
63 spillover_path: None,
64 output_summary: None,
65 is_diff: false,
66 })));
67 }
68 app
69 }
70 fn guarded(area: Rect) -> Buffer {
71 let mut buf = Buffer::empty(Rect::new(2, 3, area.width + 12, area.height + 8));
72 for cell in &mut buf.content {
73 cell.set_symbol("~")
74 .set_style(Style::default().bg(Color::Rgb(11, 23, 37)));
75 }
76 buf
77 }
78 fn geometry(app: &App) -> (Option<Rect>, usize, usize, usize, Option<Rect>) {
79 (
80 app.viewport.last_transcript_area,
81 app.viewport.last_transcript_top,
82 app.viewport.last_transcript_visible,
83 app.viewport.last_transcript_total,
84 app.viewport.jump_to_latest_button_area,
85 )
86 }
87 #[test]
88 fn mounted_transcript_complete_buffer_and_viewport_match_frozen_renderer() {
89 for &locale in Locale::shipped() {
90 for case in 0..6 {
91 let (mut current, mut old) = (app(locale, case), app(locale, case));
92 for width in [20, 40, 80, 120] {
93 for height in [3, 5, 12, 20] {
94 let area = Rect::new(7, 5, width, height);
95 let mut actual = guarded(area);
96 let mut expected = actual.clone();
97 legacy_transcript::snapshot(&mut old, area, 0).render(area, &mut expected);
98 ChatWidget::new_with_ocean_elapsed(&mut current, area, 0)
99 .render(area, &mut actual);
100 assert_eq!(
101 actual, expected,
102 "locale={locale:?} case={case} area={area:?}"
103 );
104 assert_eq!(geometry(&current), geometry(&old));
105 assert_eq!(current.history.len(), old.history.len());
106 assert_eq!(
107 current.viewport.transcript_cache.line_meta(),
108 old.viewport.transcript_cache.line_meta()
109 );
110 }
111 }
112 }
113 }
114 }
115 #[test]
116 fn mounted_transcript_stream_resize_and_scroll_lock_preserve_cache_source() {
117 let (mut current, mut old) = (app(Locale::En, 2), app(Locale::En, 2));
118 for (chunk, width, height, top) in [
119 ("First source", 80, 12, None),
120 ("First source plus 鲸鱼\n```rust\nlet x", 20, 5, None),
121 (
122 "First source plus 鲸鱼\n```rust\nlet x = 1;\n```\n[guide](https://example.test/source)",
123 40,
124 3,
125 Some(0),
126 ),
127 ("Final source and receipt", 80, 20, Some(0)),
128 ] {
129 for app in [&mut current, &mut old] {
130 app.history[1] = HistoryCell::Assistant {
131 content: chunk.into(),
132 streaming: chunk != "Final source and receipt",
133 };
134 app.bump_history_cell(1);
135 if let Some(top) = top {
136 app.viewport.transcript_scroll =
137 crate::tui::scrolling::TranscriptScroll::at_line(top);
138 app.user_scrolled_during_stream = true;
139 }
140 }
141 let area = Rect::new(7, 5, width, height);
142 let mut actual = guarded(area);
143 let mut expected = actual.clone();
144 legacy_transcript::snapshot(&mut old, area, 0).render(area, &mut expected);
145 ChatWidget::new_with_ocean_elapsed(&mut current, area, 0).render(area, &mut actual);
146 assert_eq!(actual, expected);
147 assert_eq!(geometry(&current), geometry(&old));
148 assert_eq!(
149 current.user_scrolled_during_stream,
150 old.user_scrolled_during_stream
151 );
152 assert_eq!(
153 current.viewport.transcript_cache.lines(),
154 old.viewport.transcript_cache.lines()
155 );
156 match &current.history[1] {
157 HistoryCell::Assistant { content, .. } => assert_eq!(content, chunk),
158 _ => panic!("source authority changed"),
159 }
160 }
161 }
162 #[test]
163 fn mounted_transcript_selection_preserves_native_cjk_keycap_and_style_grammar() {
164 for text in [
165 "中 cafe\u{0301} words",
166 "1\u{20e3}① 1\u{fe0f}\u{20e3} words",
167 "👩\u{200d}💻 joined emoji",
168 ] {
169 let line = Line::from(vec![
170 Span::styled(
171 text,
172 Style::default()
173 .fg(Color::Rgb(31, 43, 59))
174 .add_modifier(Modifier::ITALIC),
175 ),
176 Span::styled(" tail", Style::default().bg(Color::Rgb(71, 83, 97))),
177 ]);
178 for start in 0..8 {
179 for end in start..12 {
180 let selection = Style::default()
181 .bg(Color::Rgb(101, 113, 127))
182 .add_modifier(Modifier::REVERSED);
183 assert_eq!(
184 codewhale_ratatui::transcript_selected_spans_measured(
185 &line,
186 start,
187 end,
188 selection,
189 grapheme_display_width
190 ),
191 legacy_transcript::selected_spans(&line, start, end, selection)
192 );
193 }
194 }
195 }
196 }
197 #[test]
198 fn mounted_transcript_links_never_hyperlink_jump_chrome_or_paint_targets() {
199 let mut app = app(Locale::En, 3);
200 let area = Rect::new(7, 5, 40, 8);
201 let mut widget = ChatWidget::new_with_ocean_elapsed(&mut app, area, 0);
202 widget.lines = vec![Line::from("Read this visible guide"); 8];
203 widget.transcript_area = area;
204 widget.scrollbar = Some(codewhale_ratatui::TranscriptScrollFacts {
205 top: 0,
206 visible: 8,
207 total: 20,
208 });
209 widget.jump_to_latest_button = codewhale_ratatui::transcript_jump_rect(area, true);
210 widget.line_links = vec![
211 vec![crate::tui::osc8::LineLink {
212 col_start: 0,
213 col_end: 100,
214 target: "https://example.test/opaque-target".into()
215 }];
216 8
217 ];
218 let mut buf = guarded(area);
219 widget.render(area, &mut buf);
220 let plan = widget.viewport().plan(area);
221 let button = plan.jump.unwrap();
222 let regions = crate::tui::osc8::FRAME_LINKS.with(|regions| regions.borrow().clone());
223 assert!(!regions.is_empty());
224 for region in regions {
225 let rect = Rect::new(
226 region.col_start,
227 region.row,
228 region.col_end - region.col_start + 1,
229 1,
230 );
231 assert_eq!(rect.intersection(plan.link_area), rect);
232 assert!(rect.intersection(button).is_empty());
233 }
234 let shown: String = buf.content.iter().map(|cell| cell.symbol()).collect();
235 assert!(!shown.contains("opaque-target"));
236 assert_eq!(buf[(button.x + 1, button.y + 1)].symbol(), "↓");
237 }
238
238 lines RUST