Skip to content

Commit 5d9d0ef

Browse files
unhappychoiceclaude
andcommitted
test: add snapshot tests for session screens and refactor global dependencies
- Add snapshot tests for SessionDetailScreen and SessionDetailsDialog - Refactor SessionDetailsDialog to inject best_records via provider - Refactor BestRecordsView to accept best_records parameter in render method - Fix screen_manager render timing: render after on_pushed_from completes - Add set_selected_session_from_index helper to RecordsScreen for testing - Extend screen_snapshot_test macro to support on_pushed_from pattern - Remove unused session_detail_screen_mock 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 298f5a8 commit 5d9d0ef

14 files changed

+555
-123
lines changed

src/presentation/game/screen_manager.rs

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,6 @@ impl ScreenManager {
271271
})?;
272272
}
273273

274-
// Force immediate render of the new screen
275-
self.render_current_screen()?;
276-
277274
// Flush after initializing new screen
278275
stdout().flush().map_err(|e| {
279276
GitTypeError::TerminalError(format!("Failed to flush after screen init: {}", e))
@@ -368,31 +365,33 @@ impl ScreenManager {
368365
ScreenTransition::None => Ok(()),
369366
ScreenTransition::Push(screen_type) => {
370367
self.prepare_screen_if_needed(&screen_type)?;
371-
self.push_screen(screen_type)
368+
self.push_screen(screen_type)?;
369+
self.render_current_screen()
370+
}
371+
ScreenTransition::Pop => {
372+
self.pop_screen()?;
373+
self.render_current_screen()
372374
}
373-
ScreenTransition::Pop => self.pop_screen(),
374375
ScreenTransition::Replace(screen_type) => {
375-
// Use ScreenTransitionManager to handle transition with side effects
376376
let validated_screen_type =
377377
ScreenTransitionManager::reduce(self.current_screen_type.clone(), screen_type)?;
378378

379379
self.prepare_screen_if_needed(&validated_screen_type)?;
380-
self.set_current_screen(validated_screen_type)
380+
self.set_current_screen(validated_screen_type)?;
381+
self.render_current_screen()
381382
}
382383
ScreenTransition::PopTo(screen_type) => {
383-
// Use ScreenTransitionManager to handle transition with side effects
384384
let validated_screen_type =
385385
ScreenTransitionManager::reduce(self.current_screen_type.clone(), screen_type)?;
386386

387387
self.prepare_screen_if_needed(&validated_screen_type)?;
388-
self.pop_to_screen(validated_screen_type)
388+
self.pop_to_screen(validated_screen_type)?;
389+
self.render_current_screen()
389390
}
390391
ScreenTransition::Exit => {
391-
// If we're already on ExitSummary, mark exit requested
392392
if self.current_screen_type == ScreenType::TotalSummary {
393393
self.exit_requested = true;
394394
} else {
395-
// Otherwise, go to ExitSummary screen
396395
let _ =
397396
self.handle_transition(ScreenTransition::Replace(ScreenType::TotalSummary));
398397
}
@@ -448,33 +447,10 @@ impl ScreenManager {
448447

449448
Ok(())
450449
}
451-
ScreenType::SessionDetail => {
452-
// Configure SessionDetail screen with data from History screen
453-
self.configure_session_detail_from_history()
454-
}
455450
_ => Ok(()),
456451
}
457452
}
458453

459-
fn configure_session_detail_from_history(&mut self) -> Result<()> {
460-
// Get the selected session data from History screen and configure SessionDetail screen
461-
self.screens
462-
.get(&ScreenType::Records)
463-
.and_then(|records_screen| records_screen.as_any().downcast_ref::<RecordsScreen>())
464-
.and_then(|records| records.get_selected_session_for_detail().clone())
465-
.map(|session_data| {
466-
self.screens
467-
.get_mut(&ScreenType::SessionDetail)
468-
.and_then(|screen| screen.as_any_mut().downcast_mut::<SessionDetailScreen>())
469-
.map(|screen| screen.set_session_data(session_data))
470-
.transpose()?;
471-
Ok::<_, GitTypeError>(())
472-
})
473-
.transpose()?;
474-
475-
Ok(())
476-
}
477-
478454
/// Run the ScreenManager main loop
479455
pub fn run(&mut self) -> Result<()> {
480456
// Initialize current screen and force initial render

src/presentation/game/screens/records_screen.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ impl RecordsScreen {
183183
&self.selected_session_for_detail
184184
}
185185

186+
pub fn set_selected_session_from_index(&mut self, index: usize) {
187+
if let Some(session) = self.sessions.get(index) {
188+
self.selected_session_for_detail = Some(session.clone());
189+
}
190+
}
191+
186192
fn render_session_list(&mut self, f: &mut Frame) {
187193
let chunks = Layout::default()
188194
.direction(Direction::Vertical)

src/presentation/game/screens/session_detail_screen.rs

Lines changed: 90 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ use crate::presentation::game::views::{PerformanceMetricsView, SessionInfoView,
99
use crate::presentation::game::{Screen, ScreenDataProvider, ScreenType, UpdateStrategy};
1010
use crate::presentation::ui::Colors;
1111
use crate::{GitTypeError, Result};
12+
use crossterm::event::{KeyCode, KeyModifiers};
1213
use ratatui::{
1314
layout::{Alignment, Constraint, Direction, Layout},
1415
style::{Modifier, Style},
1516
text::{Line, Span},
1617
widgets::Paragraph,
17-
Frame,
1818
};
1919

2020
#[derive(Debug, Clone)]
@@ -60,71 +60,6 @@ impl SessionDetailScreen {
6060
event_bus,
6161
}
6262
}
63-
64-
pub fn set_session_data(&mut self, session_data: SessionDisplayData) -> Result<()> {
65-
self.session_data = session_data;
66-
67-
let session_repo = SessionRepository::new()?;
68-
self.stage_results =
69-
session_repo.get_session_stage_results(self.session_data.session.id)?;
70-
self.stage_scroll_offset = 0;
71-
72-
Ok(())
73-
}
74-
75-
fn ui(&mut self, f: &mut Frame) {
76-
let main_chunks = Layout::default()
77-
.direction(Direction::Vertical)
78-
.constraints([
79-
Constraint::Length(1),
80-
Constraint::Min(1),
81-
Constraint::Length(1),
82-
])
83-
.split(f.area());
84-
85-
let title = Paragraph::new("Session Details")
86-
.style(
87-
Style::default()
88-
.fg(Colors::info())
89-
.add_modifier(Modifier::BOLD),
90-
)
91-
.alignment(Alignment::Left);
92-
f.render_widget(title, main_chunks[0]);
93-
94-
let content_chunks = Layout::default()
95-
.direction(Direction::Vertical)
96-
.constraints([Constraint::Length(12), Constraint::Min(1)])
97-
.split(main_chunks[1]);
98-
99-
let top_chunks = Layout::default()
100-
.direction(Direction::Horizontal)
101-
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
102-
.split(content_chunks[0]);
103-
104-
SessionInfoView::render(
105-
f,
106-
top_chunks[0],
107-
&self.session_data.session,
108-
self.session_data.repository.as_ref(),
109-
);
110-
PerformanceMetricsView::render(f, top_chunks[1], self.session_data.session_result.as_ref());
111-
StageDetailsView::render(
112-
f,
113-
content_chunks[1],
114-
&self.stage_results,
115-
self.stage_scroll_offset,
116-
);
117-
118-
let controls_line = Line::from(vec![
119-
Span::styled("[↑↓/JK]", Style::default().fg(Colors::key_navigation())),
120-
Span::styled(" Scroll Stages ", Style::default().fg(Colors::text())),
121-
Span::styled("[ESC]", Style::default().fg(Colors::error())),
122-
Span::styled(" Back", Style::default().fg(Colors::text())),
123-
]);
124-
125-
let controls = Paragraph::new(controls_line).alignment(Alignment::Center);
126-
f.render_widget(controls, main_chunks[2]);
127-
}
12863
}
12964

13065
pub struct SessionDetailScreenDataProvider;
@@ -147,26 +82,48 @@ impl Screen for SessionDetailScreen {
14782
Box::new(SessionDetailScreenDataProvider)
14883
}
14984

150-
fn init_with_data(&mut self, _data: Box<dyn std::any::Any>) -> Result<()> {
85+
fn init_with_data(&mut self, data: Box<dyn std::any::Any>) -> Result<()> {
86+
let _ = data;
15187
Ok(())
15288
}
15389

15490
fn on_pushed_from(&mut self, source_screen: &dyn Screen) -> Result<()> {
155-
if let Some(records) = source_screen.as_any().downcast_ref::<RecordsScreen>() {
156-
if let Some(session_data) = records.get_selected_session_for_detail() {
157-
self.set_session_data(session_data.clone())?;
158-
return Ok(());
159-
}
91+
let records = source_screen
92+
.as_any()
93+
.downcast_ref::<RecordsScreen>()
94+
.ok_or_else(|| {
95+
GitTypeError::ScreenInitializationError(
96+
"SessionDetail must be pushed from Records screen".to_string(),
97+
)
98+
})?;
99+
100+
let session_data = records
101+
.get_selected_session_for_detail()
102+
.as_ref()
103+
.cloned()
104+
.ok_or_else(|| {
105+
GitTypeError::ScreenInitializationError(
106+
"SessionDetail requires selected session data from Records screen".to_string(),
107+
)
108+
})?;
109+
110+
let session_repo = SessionRepository::new()?;
111+
let stage_results = session_repo.get_session_stage_results(session_data.session.id)?;
112+
113+
self.session_data = session_data;
114+
self.stage_results = stage_results;
115+
self.stage_scroll_offset = 0;
116+
117+
if self.session_data.session.id == 0 {
118+
return Err(GitTypeError::ScreenInitializationError(
119+
"SessionDetail: session id cannot be 0".to_string(),
120+
));
160121
}
161122

162-
Err(GitTypeError::ScreenInitializationError(
163-
"SessionDetail must be pushed from Records screen with selected session".to_string(),
164-
))
123+
Ok(())
165124
}
166125

167126
fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) -> Result<()> {
168-
use crossterm::event::{KeyCode, KeyModifiers};
169-
170127
match key_event.code {
171128
KeyCode::Esc => {
172129
self.event_bus.publish(NavigateTo::Pop);
@@ -193,7 +150,62 @@ impl Screen for SessionDetailScreen {
193150
}
194151

195152
fn render_ratatui(&mut self, frame: &mut ratatui::Frame) -> Result<()> {
196-
self.ui(frame);
153+
let main_chunks = Layout::default()
154+
.direction(Direction::Vertical)
155+
.constraints([
156+
Constraint::Length(1),
157+
Constraint::Min(1),
158+
Constraint::Length(1),
159+
])
160+
.split(frame.area());
161+
162+
let title = Paragraph::new("Session Details")
163+
.style(
164+
Style::default()
165+
.fg(Colors::info())
166+
.add_modifier(Modifier::BOLD),
167+
)
168+
.alignment(Alignment::Left);
169+
frame.render_widget(title, main_chunks[0]);
170+
171+
let content_chunks = Layout::default()
172+
.direction(Direction::Vertical)
173+
.constraints([Constraint::Length(12), Constraint::Min(1)])
174+
.split(main_chunks[1]);
175+
176+
let top_chunks = Layout::default()
177+
.direction(Direction::Horizontal)
178+
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
179+
.split(content_chunks[0]);
180+
181+
SessionInfoView::render(
182+
frame,
183+
top_chunks[0],
184+
&self.session_data.session,
185+
self.session_data.repository.as_ref(),
186+
);
187+
PerformanceMetricsView::render(
188+
frame,
189+
top_chunks[1],
190+
self.session_data.session_result.as_ref(),
191+
);
192+
StageDetailsView::render(
193+
frame,
194+
content_chunks[1],
195+
&self.stage_results,
196+
self.stage_scroll_offset,
197+
);
198+
199+
let controls_line = Line::from(vec![
200+
Span::styled("[↑↓/JK]", Style::default().fg(Colors::key_navigation())),
201+
Span::styled(" Scroll Stages ", Style::default().fg(Colors::text())),
202+
Span::styled("[ESC]", Style::default().fg(Colors::error())),
203+
Span::styled(" Back", Style::default().fg(Colors::text())),
204+
]);
205+
206+
let controls = Paragraph::new(controls_line).alignment(Alignment::Center);
207+
frame.render_widget(controls, main_chunks[2]);
208+
197209
Ok(())
198210
}
199211

src/presentation/game/screens/session_details_dialog.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::domain::events::EventBus;
22
use crate::domain::models::SessionResult;
3-
use crate::domain::repositories::session_repository::{BestStatus, SessionRepository};
3+
use crate::domain::repositories::session_repository::{BestRecords, BestStatus, SessionRepository};
44
use crate::presentation::game::events::NavigateTo;
55
use crate::presentation::game::views::{
66
BestRecordsView, ControlsView, HeaderView, StageResultsView,
@@ -19,6 +19,7 @@ pub struct SessionDetailsDialogData {
1919
pub session_result: Option<SessionResult>,
2020
pub repo_info: Option<GitRepository>,
2121
pub best_status: Option<BestStatus>,
22+
pub best_records: Option<BestRecords>,
2223
}
2324

2425
pub struct SessionDetailsDialogDataProvider {
@@ -54,10 +55,13 @@ impl ScreenDataProvider for SessionDetailsDialogDataProvider {
5455
None
5556
};
5657

58+
let best_records = SessionRepository::get_best_records_global().ok().flatten();
59+
5760
Ok(Box::new(SessionDetailsDialogData {
5861
session_result,
5962
repo_info,
6063
best_status,
64+
best_records,
6165
}))
6266
}
6367
}
@@ -66,6 +70,7 @@ pub struct SessionDetailsDialog {
6670
session_result: Option<SessionResult>,
6771
repo_info: Option<GitRepository>,
6872
best_status: Option<BestStatus>,
73+
best_records: Option<BestRecords>,
6974
event_bus: EventBus,
7075
}
7176

@@ -75,6 +80,7 @@ impl SessionDetailsDialog {
7580
session_result: None,
7681
repo_info: None,
7782
best_status: None,
83+
best_records: None,
7884
event_bus,
7985
}
8086
}
@@ -87,7 +93,7 @@ impl SessionDetailsDialog {
8793

8894
// Calculate required content height dynamically
8995
let stage_count = session_result.stage_results.len();
90-
let best_records_lines = if let Ok(Some(_)) = SessionRepository::get_best_records_global() {
96+
let best_records_lines = if self.best_records.is_some() {
9197
5 // Header + 3 records + padding
9298
} else {
9399
3 // Just header and no records message
@@ -142,11 +148,12 @@ impl SessionDetailsDialog {
142148
])
143149
.split(main_chunks[2]);
144150

145-
BestRecordsView::render_with_best_status(
151+
BestRecordsView::render(
146152
f,
147153
content_chunks[0],
148154
session_result,
149155
self.best_status.as_ref(),
156+
self.best_records.as_ref(),
150157
);
151158
StageResultsView::render(f, content_chunks[1], session_result, &self.repo_info);
152159
ControlsView::render(f, main_chunks[4]);
@@ -174,6 +181,7 @@ impl Screen for SessionDetailsDialog {
174181
self.session_result = dialog_data.session_result;
175182
self.repo_info = dialog_data.repo_info;
176183
self.best_status = dialog_data.best_status;
184+
self.best_records = dialog_data.best_records;
177185

178186
Ok(())
179187
}

0 commit comments

Comments
 (0)