Skip to content

Commit c945e13

Browse files
committed
test: add presentation layer tests
- Expand sharing_tests from 2 to 13 tests - Add text_processor edge case tests - Expand colors_tests from 2 to 11 tests - Expand gradation_text_tests from 14 to 39 tests - Add screen_transition_manager tests for all valid transitions
1 parent 89d4228 commit c945e13

File tree

6 files changed

+1282
-1
lines changed

6 files changed

+1282
-1
lines changed

tests/unit/presentation/game/text_processor_tests.rs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,181 @@ fn is_at_end_of_line_content_at_end_of_text() {
191191
let result = TextProcessor::is_at_end_of_line_content(text, 5, &line_starts, &[]);
192192
assert!(!result); // Out of bounds
193193
}
194+
195+
// ============================================
196+
// should_skip_character - middle newline
197+
// ============================================
198+
199+
#[test]
200+
fn should_skip_character_newline_in_middle() {
201+
let text = "hello\nworld";
202+
let line_starts = TextProcessor::calculate_line_starts(text);
203+
// Newline at position 5 is NOT the final newline, so should NOT be skipped
204+
let result = TextProcessor::should_skip_character(text, 5, &line_starts, &[]);
205+
assert!(!result);
206+
}
207+
208+
#[test]
209+
fn should_skip_character_out_of_bounds() {
210+
let text = "hello";
211+
let line_starts = TextProcessor::calculate_line_starts(text);
212+
let result = TextProcessor::should_skip_character(text, 100, &line_starts, &[]);
213+
assert!(!result);
214+
}
215+
216+
// ============================================
217+
// should_skip_character - whitespace before comment
218+
// ============================================
219+
220+
#[test]
221+
fn should_skip_character_whitespace_before_comment() {
222+
// "code // comment" where comment starts at position 6
223+
let text = "code // comment";
224+
let line_starts = TextProcessor::calculate_line_starts(text);
225+
let comment_ranges = vec![(6, 16)];
226+
// Position 4 is space before the comment
227+
let result = TextProcessor::should_skip_character(text, 4, &line_starts, &comment_ranges);
228+
assert!(result); // whitespace before comment should be skipped
229+
}
230+
231+
#[test]
232+
fn should_skip_character_whitespace_not_before_comment() {
233+
let text = "code more";
234+
let line_starts = TextProcessor::calculate_line_starts(text);
235+
// Position 4 is space, but no comment follows
236+
let result = TextProcessor::should_skip_character(text, 4, &line_starts, &[]);
237+
assert!(!result);
238+
}
239+
240+
// ============================================
241+
// process_challenge_text_with_comment_mapping - with comment ranges
242+
// ============================================
243+
244+
#[test]
245+
fn process_with_comment_mapping_maps_comments_correctly() {
246+
let text = "code // comment\nnext line";
247+
let comment_ranges = vec![(5, 15)];
248+
let (processed, mapped_ranges) =
249+
TextProcessor::process_challenge_text_with_comment_mapping(text, &comment_ranges);
250+
assert_eq!(processed, "code // comment\nnext line");
251+
assert!(!mapped_ranges.is_empty());
252+
// The comment range should be mapped to the same positions in processed text
253+
let (start, end) = mapped_ranges[0];
254+
assert_eq!(start, 5);
255+
assert!(end > start);
256+
}
257+
258+
#[test]
259+
fn process_with_comment_mapping_removes_empty_lines_and_adjusts_ranges() {
260+
let text = "code // comment\n\nnext line";
261+
let comment_ranges = vec![(5, 15)];
262+
let (processed, mapped_ranges) =
263+
TextProcessor::process_challenge_text_with_comment_mapping(text, &comment_ranges);
264+
// Empty line should be removed
265+
assert_eq!(processed, "code // comment\nnext line");
266+
// Comment range should still be valid in the processed text
267+
assert!(!mapped_ranges.is_empty());
268+
}
269+
270+
#[test]
271+
fn process_with_comment_mapping_comment_in_removed_line() {
272+
// Comment is entirely in an empty/removed section
273+
let text = "\n// only comment\ncode";
274+
// First line is empty, gets removed. Comment is on second line (positions 1-16)
275+
let comment_ranges = vec![(1, 17)];
276+
let (processed, mapped_ranges) =
277+
TextProcessor::process_challenge_text_with_comment_mapping(text, &comment_ranges);
278+
// "// only comment" line is not empty so should be kept
279+
assert!(processed.contains("code"));
280+
// mapped_ranges should still have the comment
281+
let _ = mapped_ranges; // just checking it doesn't panic
282+
}
283+
284+
#[test]
285+
fn process_with_comment_mapping_preserve_empty_with_comments() {
286+
let text = "code // comment\n\nnext line";
287+
let comment_ranges = vec![(5, 15)];
288+
let (processed, mapped_ranges) =
289+
TextProcessor::process_challenge_text_with_comment_mapping_preserve_empty(
290+
text,
291+
&comment_ranges,
292+
true,
293+
);
294+
// With preserve_empty = true, empty line should be kept
295+
assert!(processed.lines().count() >= 3);
296+
assert!(!mapped_ranges.is_empty());
297+
}
298+
299+
// ============================================
300+
// is_at_end_of_line_content - with comment ranges
301+
// ============================================
302+
303+
#[test]
304+
fn is_at_end_of_line_content_before_comment() {
305+
let text = "code // comment\nnext";
306+
let line_starts = TextProcessor::calculate_line_starts(text);
307+
let comment_ranges = vec![(5, 15)];
308+
// Position 4 is space before comment - rest of line is comment only
309+
let result = TextProcessor::is_at_end_of_line_content(text, 4, &line_starts, &comment_ranges);
310+
assert!(result);
311+
}
312+
313+
#[test]
314+
fn is_at_end_of_line_content_before_code() {
315+
let text = "code more_code";
316+
let line_starts = TextProcessor::calculate_line_starts(text);
317+
let result = TextProcessor::is_at_end_of_line_content(text, 4, &line_starts, &[]);
318+
assert!(!result);
319+
}
320+
321+
// ============================================
322+
// is_rest_of_line_comment_only - edge cases
323+
// ============================================
324+
325+
#[test]
326+
fn is_rest_of_line_comment_only_whitespace_then_comment() {
327+
let text = "code // comment";
328+
let comment_ranges = vec![(7, 17)];
329+
// Position 4 has spaces then comment
330+
let result = TextProcessor::is_rest_of_line_comment_only(text, 4, &comment_ranges);
331+
assert!(result);
332+
}
333+
334+
#[test]
335+
fn is_rest_of_line_comment_only_out_of_bounds() {
336+
let result = TextProcessor::is_rest_of_line_comment_only("hello", 100, &[]);
337+
assert!(!result);
338+
}
339+
340+
#[test]
341+
fn is_rest_of_line_comment_only_at_newline_boundary() {
342+
let text = "code // comment\nnext";
343+
let comment_ranges = vec![(5, 15)];
344+
let result = TextProcessor::is_rest_of_line_comment_only(text, 5, &comment_ranges);
345+
assert!(result);
346+
}
347+
348+
// ============================================
349+
// process_challenge_text - more edge cases
350+
// ============================================
351+
352+
#[test]
353+
fn process_challenge_text_only_whitespace_lines() {
354+
let text = " \n \n ";
355+
let result = TextProcessor::process_challenge_text(text);
356+
assert_eq!(result, "");
357+
}
358+
359+
#[test]
360+
fn process_challenge_text_mixed_empty_and_content() {
361+
let text = "\nline1\n\n\nline2\n";
362+
let result = TextProcessor::process_challenge_text(text);
363+
assert_eq!(result, "line1\nline2");
364+
}
365+
366+
#[test]
367+
fn should_skip_final_newline_non_newline_char() {
368+
let text = "hello";
369+
let result = TextProcessor::should_skip_final_newline(text, 4);
370+
assert!(!result); // 'o' is not a newline
371+
}

tests/unit/presentation/sharing_tests.rs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::collections::HashSet;
22

3-
use gittype::presentation::sharing::SharingPlatform;
3+
use gittype::domain::models::{GitRepository, SessionResult};
4+
use gittype::presentation::sharing::{SharingPlatform, SharingService};
45

56
#[test]
67
fn sharing_platform_all_lists_every_variant_once() {
@@ -22,3 +23,146 @@ fn sharing_platform_name_matches_variant() {
2223
assert_eq!(SharingPlatform::LinkedIn.name(), "LinkedIn");
2324
assert_eq!(SharingPlatform::Facebook.name(), "Facebook");
2425
}
26+
27+
// ---------------------------------------------------------------------------
28+
// Helpers
29+
// ---------------------------------------------------------------------------
30+
fn make_metrics(
31+
score: f64,
32+
cpm: f64,
33+
valid_mistakes: usize,
34+
invalid_mistakes: usize,
35+
) -> SessionResult {
36+
let mut result = SessionResult::new();
37+
result.session_score = score;
38+
result.overall_cpm = cpm;
39+
result.valid_mistakes = valid_mistakes;
40+
result.invalid_mistakes = invalid_mistakes;
41+
result
42+
}
43+
44+
fn make_repo() -> GitRepository {
45+
GitRepository {
46+
user_name: "testuser".to_string(),
47+
repository_name: "testrepo".to_string(),
48+
remote_url: "https://github.com/testuser/testrepo".to_string(),
49+
branch: Some("main".to_string()),
50+
commit_hash: Some("abc".to_string()),
51+
is_dirty: false,
52+
root_path: None,
53+
}
54+
}
55+
56+
// ---------------------------------------------------------------------------
57+
// create_share_text tests
58+
// ---------------------------------------------------------------------------
59+
#[test]
60+
fn create_share_text_without_repo() {
61+
let metrics = make_metrics(150.0, 300.0, 3, 2);
62+
let text = SharingService::create_share_text(&metrics, &None);
63+
64+
assert!(text.contains("150"), "should contain score");
65+
assert!(text.contains("300"), "should contain cpm");
66+
assert!(text.contains("5"), "should contain total mistakes (3+2)");
67+
assert!(text.contains("#gittype"));
68+
assert!(text.contains("github.com/unhappychoice/gittype"));
69+
// Should NOT contain repo info
70+
assert!(!text.contains("testuser"));
71+
}
72+
73+
#[test]
74+
fn create_share_text_with_repo() {
75+
let metrics = make_metrics(200.0, 400.0, 1, 0);
76+
let repo = make_repo();
77+
let text = SharingService::create_share_text(&metrics, &Some(repo));
78+
79+
assert!(text.contains("200"), "should contain score");
80+
assert!(text.contains("400"), "should contain cpm");
81+
assert!(
82+
text.contains("testuser/testrepo"),
83+
"should contain repo info"
84+
);
85+
assert!(text.contains("#gittype"));
86+
}
87+
88+
// ---------------------------------------------------------------------------
89+
// generate_share_url tests — one per platform
90+
// ---------------------------------------------------------------------------
91+
#[test]
92+
fn generate_share_url_x() {
93+
let metrics = make_metrics(100.0, 250.0, 2, 1);
94+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::X, &None);
95+
assert!(url.starts_with("https://x.com/intent/tweet?text="));
96+
assert!(url.contains("gittype"));
97+
}
98+
99+
#[test]
100+
fn generate_share_url_reddit() {
101+
let metrics = make_metrics(100.0, 250.0, 2, 1);
102+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::Reddit, &None);
103+
assert!(url.starts_with("https://www.reddit.com/submit?"));
104+
assert!(url.contains("title="));
105+
assert!(url.contains("selftext=true"));
106+
assert!(url.contains("text="));
107+
}
108+
109+
#[test]
110+
fn generate_share_url_linkedin() {
111+
let metrics = make_metrics(100.0, 250.0, 2, 1);
112+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::LinkedIn, &None);
113+
assert!(url.starts_with("https://www.linkedin.com/feed/"));
114+
assert!(url.contains("shareActive=true"));
115+
}
116+
117+
#[test]
118+
fn generate_share_url_facebook() {
119+
let metrics = make_metrics(100.0, 250.0, 2, 1);
120+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::Facebook, &None);
121+
assert!(url.starts_with("https://www.facebook.com/sharer/"));
122+
assert!(url.contains("quote="));
123+
}
124+
125+
#[test]
126+
fn generate_share_url_x_with_repo() {
127+
let metrics = make_metrics(300.0, 600.0, 0, 0);
128+
let repo = make_repo();
129+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::X, &Some(repo));
130+
assert!(url.starts_with("https://x.com/intent/tweet?text="));
131+
// URL-encoded repo name should be present
132+
assert!(url.contains("testuser"));
133+
}
134+
135+
#[test]
136+
fn generate_share_url_reddit_with_repo() {
137+
let metrics = make_metrics(300.0, 600.0, 0, 0);
138+
let repo = make_repo();
139+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::Reddit, &Some(repo));
140+
assert!(url.contains("reddit.com"));
141+
assert!(url.contains("title="));
142+
}
143+
144+
#[test]
145+
fn generate_share_url_linkedin_with_repo() {
146+
let metrics = make_metrics(300.0, 600.0, 0, 0);
147+
let repo = make_repo();
148+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::LinkedIn, &Some(repo));
149+
assert!(url.contains("linkedin.com"));
150+
}
151+
152+
#[test]
153+
fn generate_share_url_facebook_with_repo() {
154+
let metrics = make_metrics(300.0, 600.0, 0, 0);
155+
let repo = make_repo();
156+
let url = SharingService::generate_share_url(&metrics, &SharingPlatform::Facebook, &Some(repo));
157+
assert!(url.contains("facebook.com"));
158+
}
159+
160+
// ---------------------------------------------------------------------------
161+
// SharingPlatform Clone + Debug
162+
// ---------------------------------------------------------------------------
163+
#[test]
164+
fn sharing_platform_clone() {
165+
let p = SharingPlatform::X;
166+
let p2 = p.clone();
167+
assert_eq!(p.name(), p2.name());
168+
}

tests/unit/presentation/tui/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod screen_manager_tests;
2+
pub mod screen_transition_manager_tests;

0 commit comments

Comments
 (0)