lean_ctx/core/patterns/
playwright.rs1use regex::Regex;
2use std::sync::OnceLock;
3
4static PW_FAILED_RE: OnceLock<Regex> = OnceLock::new();
5
6fn pw_failed_re() -> &'static Regex {
7 PW_FAILED_RE.get_or_init(|| Regex::new(r"^\s+\d+\)\s+(.+)$").unwrap())
8}
9
10pub fn compress(command: &str, output: &str) -> Option<String> {
11 if command.contains("cypress") {
12 return Some(compress_cypress(output));
13 }
14 Some(compress_playwright(output))
15}
16
17fn compress_playwright(output: &str) -> String {
18 let trimmed = output.trim();
19 if trimmed.is_empty() {
20 return "ok".to_string();
21 }
22
23 let mut passed = 0u32;
24 let mut failed = 0u32;
25 let mut skipped = 0u32;
26 let mut failed_names = Vec::new();
27 let mut duration = String::new();
28
29 for line in trimmed.lines() {
30 let l = line.trim().to_lowercase();
31 if l.contains("passed") {
32 if let Some(n) = extract_number(&l, "passed") {
33 passed = n;
34 }
35 }
36 if l.contains("failed") {
37 if let Some(n) = extract_number(&l, "failed") {
38 failed = n;
39 }
40 }
41 if l.contains("skipped") {
42 if let Some(n) = extract_number(&l, "skipped") {
43 skipped = n;
44 }
45 }
46 if let Some(caps) = pw_failed_re().captures(line) {
47 failed_names.push(caps[1].trim().to_string());
48 }
49 if l.contains("finished in") || l.contains("duration") {
50 duration = line.trim().to_string();
51 }
52 }
53
54 let total = passed + failed + skipped;
55 if total == 0 {
56 return compact_output(trimmed, 10);
57 }
58
59 let mut parts = Vec::new();
60 parts.push(format!(
61 "{total} tests: {passed} passed, {failed} failed, {skipped} skipped"
62 ));
63
64 if !failed_names.is_empty() {
65 parts.push("failed:".to_string());
66 for name in failed_names.iter().take(10) {
67 parts.push(format!(" {name}"));
68 }
69 if failed_names.len() > 10 {
70 parts.push(format!(" ... +{} more", failed_names.len() - 10));
71 }
72 }
73
74 if !duration.is_empty() {
75 parts.push(duration);
76 }
77
78 parts.join("\n")
79}
80
81fn compress_cypress(output: &str) -> String {
82 let trimmed = output.trim();
83 if trimmed.is_empty() {
84 return "ok".to_string();
85 }
86
87 let mut passed = 0u32;
88 let mut failed = 0u32;
89 let mut pending = 0u32;
90
91 for line in trimmed.lines() {
92 let l = line.trim().to_lowercase();
93 if l.contains("passing") {
94 passed += extract_first_number(&l);
95 }
96 if l.contains("failing") {
97 failed += extract_first_number(&l);
98 }
99 if l.contains("pending") {
100 pending += extract_first_number(&l);
101 }
102 }
103
104 let total = passed + failed + pending;
105 if total == 0 {
106 return compact_output(trimmed, 10);
107 }
108
109 format!("{total} tests: {passed} passed, {failed} failed, {pending} pending")
110}
111
112fn extract_number(line: &str, keyword: &str) -> Option<u32> {
113 let pos = line.find(keyword)?;
114 let before = &line[..pos];
115 before.split_whitespace().last()?.parse().ok()
116}
117
118fn extract_first_number(line: &str) -> u32 {
119 for word in line.split_whitespace() {
120 if let Ok(n) = word.parse::<u32>() {
121 return n;
122 }
123 }
124 0
125}
126
127fn compact_output(text: &str, max: usize) -> String {
128 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
129 if lines.len() <= max {
130 return lines.join("\n");
131 }
132 format!(
133 "{}\n... ({} more lines)",
134 lines[..max].join("\n"),
135 lines.len() - max
136 )
137}