Skip to main content

lean_ctx/core/patterns/
playwright.rs

1use 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}