Skip to main content

lean_ctx/core/patterns/
playwright.rs

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