Skip to main content

victauri_test/
assertions.rs

1use serde_json::Value;
2
3use crate::VictauriClient;
4use crate::error::TestError;
5
6/// Fluent assertion builder for full-stack Tauri test verification.
7///
8/// Collects multiple checks (DOM, IPC, network, state) and reports all
9/// failures together rather than stopping at the first one.
10///
11/// # Example
12///
13/// ```rust,ignore
14/// let report = client.verify()
15///     .has_text("Hello, World!")
16///     .has_no_text("Error")
17///     .ipc_was_called("greet")
18///     .ipc_was_called_with("greet", json!({"name": "World"}))
19///     .ipc_was_not_called("delete_account")
20///     .no_console_errors()
21///     .run()
22///     .await
23///     .unwrap();
24///
25/// report.assert_all_passed();
26/// ```
27pub struct VerifyBuilder<'a> {
28    client: &'a mut VictauriClient,
29    checks: Vec<Check>,
30}
31
32enum Check {
33    HasText(String),
34    HasNoText(String),
35    IpcWasCalled(String),
36    IpcWasCalledWith(String, Value),
37    IpcWasNotCalled(String),
38    NetworkRequest {
39        method: Option<String>,
40        url_contains: String,
41    },
42    NoNetworkRequest {
43        url_contains: String,
44    },
45    NoConsoleErrors,
46    StateMatches {
47        frontend_expr: String,
48        backend_state: Value,
49    },
50    IpcHealthy,
51    NoGhostCommands,
52    CoverageAbove(f64),
53}
54
55/// A single check result — pass or fail with context.
56#[derive(Debug, Clone)]
57pub struct CheckResult {
58    /// Human-readable description of what was checked.
59    pub description: String,
60    /// Whether the check passed.
61    pub passed: bool,
62    /// Failure detail (empty if passed).
63    pub detail: String,
64}
65
66/// Collection of check results from a `verify()` run.
67#[derive(Debug)]
68pub struct VerifyReport {
69    /// Individual check results in order.
70    pub results: Vec<CheckResult>,
71}
72
73impl VerifyReport {
74    /// Returns true if all checks passed.
75    #[must_use]
76    pub fn all_passed(&self) -> bool {
77        self.results.iter().all(|r| r.passed)
78    }
79
80    /// Returns only the failed checks.
81    #[must_use]
82    pub fn failures(&self) -> Vec<&CheckResult> {
83        self.results.iter().filter(|r| !r.passed).collect()
84    }
85
86    /// Converts this report to a `JUnit` XML [`JunitReport`](crate::reporting::JunitReport).
87    #[must_use]
88    pub fn to_junit(
89        &self,
90        suite_name: &str,
91        duration: std::time::Duration,
92    ) -> crate::reporting::JunitReport {
93        crate::reporting::JunitReport::from_verify_report(self, suite_name, duration)
94    }
95
96    /// Panics with a formatted report if any check failed.
97    ///
98    /// # Panics
99    ///
100    /// Panics if any check in the report did not pass.
101    pub fn assert_all_passed(&self) {
102        if self.all_passed() {
103            return;
104        }
105        let failures: Vec<String> = self
106            .failures()
107            .iter()
108            .enumerate()
109            .map(|(i, f)| format!("  {}. {} — {}", i + 1, f.description, f.detail))
110            .collect();
111        panic!(
112            "verify() failed ({}/{} checks passed):\n{}",
113            self.results.len() - failures.len(),
114            self.results.len(),
115            failures.join("\n")
116        );
117    }
118}
119
120impl<'a> VerifyBuilder<'a> {
121    pub(crate) fn new(client: &'a mut VictauriClient) -> Self {
122        Self {
123            client,
124            checks: Vec::new(),
125        }
126    }
127
128    /// Assert that the page currently contains the given text.
129    #[must_use]
130    pub fn has_text(mut self, text: &str) -> Self {
131        self.checks.push(Check::HasText(text.to_string()));
132        self
133    }
134
135    /// Assert that the page does NOT contain the given text.
136    #[must_use]
137    pub fn has_no_text(mut self, text: &str) -> Self {
138        self.checks.push(Check::HasNoText(text.to_string()));
139        self
140    }
141
142    /// Assert that an IPC command was called at least once.
143    #[must_use]
144    pub fn ipc_was_called(mut self, command: &str) -> Self {
145        self.checks.push(Check::IpcWasCalled(command.to_string()));
146        self
147    }
148
149    /// Assert that an IPC command was called with specific arguments.
150    #[must_use]
151    pub fn ipc_was_called_with(mut self, command: &str, args: Value) -> Self {
152        self.checks
153            .push(Check::IpcWasCalledWith(command.to_string(), args));
154        self
155    }
156
157    /// Assert that an IPC command was never called.
158    #[must_use]
159    pub fn ipc_was_not_called(mut self, command: &str) -> Self {
160        self.checks
161            .push(Check::IpcWasNotCalled(command.to_string()));
162        self
163    }
164
165    /// Assert a network request was made matching the given URL substring.
166    #[must_use]
167    pub fn network_request(mut self, method: Option<&str>, url_contains: &str) -> Self {
168        self.checks.push(Check::NetworkRequest {
169            method: method.map(String::from),
170            url_contains: url_contains.to_string(),
171        });
172        self
173    }
174
175    /// Assert NO network request was made matching the given URL substring.
176    #[must_use]
177    pub fn no_network_request(mut self, url_contains: &str) -> Self {
178        self.checks.push(Check::NoNetworkRequest {
179            url_contains: url_contains.to_string(),
180        });
181        self
182    }
183
184    /// Assert that no console errors were logged.
185    #[must_use]
186    pub fn no_console_errors(mut self) -> Self {
187        self.checks.push(Check::NoConsoleErrors);
188        self
189    }
190
191    /// Assert that frontend state matches backend state.
192    #[must_use]
193    pub fn state_matches(mut self, frontend_expr: &str, backend_state: Value) -> Self {
194        self.checks.push(Check::StateMatches {
195            frontend_expr: frontend_expr.to_string(),
196            backend_state,
197        });
198        self
199    }
200
201    /// Assert that IPC integrity is healthy (no stale/errored calls).
202    #[must_use]
203    pub fn ipc_healthy(mut self) -> Self {
204        self.checks.push(Check::IpcHealthy);
205        self
206    }
207
208    /// Assert that there are no ghost commands.
209    #[must_use]
210    pub fn no_ghost_commands(mut self) -> Self {
211        self.checks.push(Check::NoGhostCommands);
212        self
213    }
214
215    /// Assert that IPC command coverage meets the given threshold percentage.
216    ///
217    /// Queries the command registry and IPC log to compute how many registered
218    /// commands have been exercised, then checks whether the coverage percentage
219    /// is at or above `threshold`.
220    #[must_use]
221    pub fn coverage_above(mut self, threshold: f64) -> Self {
222        self.checks.push(Check::CoverageAbove(threshold));
223        self
224    }
225
226    /// Execute all queued checks and return the report.
227    ///
228    /// # Errors
229    ///
230    /// Returns [`TestError`] only on transport/connection failures.
231    /// Check failures are reported in the [`VerifyReport`], not as errors.
232    pub async fn run(self) -> Result<VerifyReport, TestError> {
233        let client = self.client;
234        let mut results = Vec::with_capacity(self.checks.len());
235
236        for check in self.checks {
237            let result = run_check(client, &check).await?;
238            results.push(result);
239        }
240
241        Ok(VerifyReport { results })
242    }
243}
244
245async fn run_check(client: &mut VictauriClient, check: &Check) -> Result<CheckResult, TestError> {
246    match check {
247        Check::HasText(text) => {
248            let snap = client.dom_snapshot().await?;
249            let snap_str = serde_json::to_string(&snap).unwrap_or_default();
250            let found = snap_str.contains(text.as_str());
251            Ok(CheckResult {
252                description: format!("page contains \"{text}\""),
253                passed: found,
254                detail: if found {
255                    String::new()
256                } else {
257                    format!("text \"{text}\" not found in DOM")
258                },
259            })
260        }
261        Check::HasNoText(text) => {
262            let snap = client.dom_snapshot().await?;
263            let snap_str = serde_json::to_string(&snap).unwrap_or_default();
264            let found = snap_str.contains(text.as_str());
265            Ok(CheckResult {
266                description: format!("page does NOT contain \"{text}\""),
267                passed: !found,
268                detail: if found {
269                    format!("text \"{text}\" was found in DOM but shouldn't be")
270                } else {
271                    String::new()
272                },
273            })
274        }
275        Check::IpcWasCalled(command) => {
276            let log = client.get_ipc_log(None).await?;
277            let found = ipc_log_contains_command(&log, command);
278            Ok(CheckResult {
279                description: format!("IPC command \"{command}\" was called"),
280                passed: found,
281                detail: if found {
282                    String::new()
283                } else {
284                    format!("command \"{command}\" not found in IPC log")
285                },
286            })
287        }
288        Check::IpcWasCalledWith(command, expected_args) => {
289            let log = client.get_ipc_log(None).await?;
290            let (found, actual_args) = ipc_log_find_with_args(&log, command, expected_args);
291            Ok(CheckResult {
292                description: format!("IPC \"{command}\" called with {expected_args}"),
293                passed: found,
294                detail: if found {
295                    String::new()
296                } else if let Some(actual) = actual_args {
297                    format!("command called but with args: {actual}")
298                } else {
299                    format!("command \"{command}\" not found in IPC log")
300                },
301            })
302        }
303        Check::IpcWasNotCalled(command) => {
304            let log = client.get_ipc_log(None).await?;
305            let found = ipc_log_contains_command(&log, command);
306            Ok(CheckResult {
307                description: format!("IPC command \"{command}\" was NOT called"),
308                passed: !found,
309                detail: if found {
310                    format!("command \"{command}\" WAS called but shouldn't have been")
311                } else {
312                    String::new()
313                },
314            })
315        }
316        Check::NetworkRequest {
317            method,
318            url_contains,
319        } => {
320            let log = client.logs("network", None).await?;
321            let found = network_log_matches(&log, method.as_deref(), url_contains);
322            let desc = match method {
323                Some(m) => format!("network {m} request to \"*{url_contains}*\""),
324                None => format!("network request to \"*{url_contains}*\""),
325            };
326            Ok(CheckResult {
327                description: desc,
328                passed: found,
329                detail: if found {
330                    String::new()
331                } else {
332                    "no matching network request found".to_string()
333                },
334            })
335        }
336        Check::NoNetworkRequest { url_contains } => {
337            let log = client.logs("network", None).await?;
338            let found = network_log_matches(&log, None, url_contains);
339            Ok(CheckResult {
340                description: format!("NO network request to \"*{url_contains}*\""),
341                passed: !found,
342                detail: if found {
343                    format!("found network request matching \"{url_contains}\" but shouldn't have")
344                } else {
345                    String::new()
346                },
347            })
348        }
349        Check::NoConsoleErrors => {
350            let log = client.logs("console", None).await?;
351            let errors = console_log_errors(&log);
352            Ok(CheckResult {
353                description: "no console errors".to_string(),
354                passed: errors.is_empty(),
355                detail: if errors.is_empty() {
356                    String::new()
357                } else {
358                    format!("{} error(s): {}", errors.len(), errors.join("; "))
359                },
360            })
361        }
362        Check::StateMatches {
363            frontend_expr,
364            backend_state,
365        } => {
366            let result = client
367                .verify_state(frontend_expr, backend_state.clone())
368                .await?;
369            let passed = result
370                .get("passed")
371                .and_then(Value::as_bool)
372                .unwrap_or(false);
373            Ok(CheckResult {
374                description: format!("state matches ({frontend_expr})"),
375                passed,
376                detail: if passed {
377                    String::new()
378                } else {
379                    let divs = result.get("divergences").cloned().unwrap_or(Value::Null);
380                    format!("divergences: {divs}")
381                },
382            })
383        }
384        Check::IpcHealthy => {
385            let result = client.check_ipc_integrity().await?;
386            let healthy = result
387                .get("healthy")
388                .and_then(Value::as_bool)
389                .unwrap_or(false);
390            Ok(CheckResult {
391                description: "IPC integrity healthy".to_string(),
392                passed: healthy,
393                detail: if healthy {
394                    String::new()
395                } else {
396                    serde_json::to_string(&result).unwrap_or_default()
397                },
398            })
399        }
400        Check::NoGhostCommands => {
401            let result = client.detect_ghost_commands().await?;
402            let ghosts = result
403                .get("ghost_commands")
404                .and_then(Value::as_array)
405                .map_or(0, Vec::len);
406            Ok(CheckResult {
407                description: "no ghost commands".to_string(),
408                passed: ghosts == 0,
409                detail: if ghosts == 0 {
410                    String::new()
411                } else {
412                    format!("{ghosts} ghost command(s) found")
413                },
414            })
415        }
416        Check::CoverageAbove(threshold) => {
417            let report = crate::coverage::coverage_report(client).await?;
418            let passed = report.meets_threshold(*threshold);
419            Ok(CheckResult {
420                description: format!(
421                    "IPC coverage >= {threshold:.1}% (actual: {:.1}%)",
422                    report.coverage_percentage
423                ),
424                passed,
425                detail: if passed {
426                    String::new()
427                } else {
428                    format!(
429                        "coverage {:.1}% is below threshold {threshold:.1}%",
430                        report.coverage_percentage
431                    )
432                },
433            })
434        }
435    }
436}
437
438fn ipc_log_contains_command(log: &Value, command: &str) -> bool {
439    if let Some(arr) = log.as_array() {
440        return arr.iter().any(|entry| {
441            entry
442                .get("command")
443                .and_then(Value::as_str)
444                .is_some_and(|c| c == command)
445        });
446    }
447    if let Some(entries) = log.get("entries").and_then(Value::as_array) {
448        return entries.iter().any(|entry| {
449            entry
450                .get("command")
451                .and_then(Value::as_str)
452                .is_some_and(|c| c == command)
453        });
454    }
455    false
456}
457
458fn ipc_log_find_with_args(
459    log: &Value,
460    command: &str,
461    expected_args: &Value,
462) -> (bool, Option<Value>) {
463    let entries = if let Some(arr) = log.as_array() {
464        arr.clone()
465    } else if let Some(entries) = log.get("entries").and_then(Value::as_array) {
466        entries.clone()
467    } else {
468        return (false, None);
469    };
470
471    let mut last_args = None;
472    for entry in &entries {
473        let cmd = entry.get("command").and_then(Value::as_str).unwrap_or("");
474        if cmd != command {
475            continue;
476        }
477        let args = entry.get("args").or_else(|| entry.get("request_body"));
478        if let Some(args) = args {
479            if args_match(args, expected_args) {
480                return (true, None);
481            }
482            last_args = Some(args.clone());
483        } else if expected_args.is_null()
484            || expected_args == &Value::Object(serde_json::Map::default())
485        {
486            return (true, None);
487        }
488    }
489    (false, last_args)
490}
491
492fn args_match(actual: &Value, expected: &Value) -> bool {
493    match expected {
494        Value::Object(exp_map) => {
495            let Some(actual_map) = actual.as_object() else {
496                return false;
497            };
498            exp_map
499                .iter()
500                .all(|(k, v)| actual_map.get(k).is_some_and(|av| av == v))
501        }
502        _ => actual == expected,
503    }
504}
505
506fn network_log_matches(log: &Value, method: Option<&str>, url_contains: &str) -> bool {
507    let entries = if let Some(arr) = log.as_array() {
508        arr.as_slice()
509    } else if let Some(entries) = log.get("entries").and_then(Value::as_array) {
510        entries.as_slice()
511    } else {
512        return false;
513    };
514
515    entries.iter().any(|entry| {
516        let url = entry.get("url").and_then(Value::as_str).unwrap_or("");
517        if !url.contains(url_contains) {
518            return false;
519        }
520        if let Some(m) = method {
521            let req_method = entry.get("method").and_then(Value::as_str).unwrap_or("");
522            return req_method.eq_ignore_ascii_case(m);
523        }
524        true
525    })
526}
527
528fn console_log_errors(log: &Value) -> Vec<String> {
529    let entries = if let Some(arr) = log.as_array() {
530        arr.as_slice()
531    } else if let Some(entries) = log.get("entries").and_then(Value::as_array) {
532        entries.as_slice()
533    } else {
534        return Vec::new();
535    };
536
537    entries
538        .iter()
539        .filter_map(|entry| {
540            let level = entry.get("level").and_then(Value::as_str).unwrap_or("");
541            if level == "error" {
542                let msg = entry
543                    .get("message")
544                    .and_then(Value::as_str)
545                    .unwrap_or("(no message)")
546                    .to_string();
547                Some(msg)
548            } else {
549                None
550            }
551        })
552        .collect()
553}
554
555// ── Standalone IPC assertion functions ──────────────────────────────────────
556
557/// Assert that a specific IPC command was called at least once.
558///
559/// # Panics
560///
561/// Panics if the command was not found in the IPC log.
562///
563/// # Examples
564///
565/// ```
566/// use serde_json::json;
567///
568/// let log = json!([{"command": "save_settings", "args": {"theme": "dark"}}]);
569/// victauri_test::assert_ipc_called(&log, "save_settings");
570/// ```
571pub fn assert_ipc_called(log: &Value, command: &str) {
572    assert!(
573        ipc_log_contains_command(log, command),
574        "expected IPC command \"{command}\" to have been called, but it was not found in the log"
575    );
576}
577
578/// Assert that a specific IPC command was called with the given arguments.
579///
580/// Uses partial matching — the expected args only need to be a subset of actual args.
581///
582/// # Panics
583///
584/// Panics if the command was not called with matching arguments.
585///
586/// # Examples
587///
588/// ```
589/// use serde_json::json;
590///
591/// let log = json!([{"command": "save_settings", "args": {"theme": "dark", "lang": "en"}}]);
592/// victauri_test::assert_ipc_called_with(&log, "save_settings", &json!({"theme": "dark"}));
593/// ```
594pub fn assert_ipc_called_with(log: &Value, command: &str, expected_args: &Value) {
595    let (found, actual_args) = ipc_log_find_with_args(log, command, expected_args);
596    if !found {
597        if let Some(actual) = actual_args {
598            panic!(
599                "IPC command \"{command}\" was called but with different args:\n  expected: {expected_args}\n  actual:   {actual}"
600            );
601        } else {
602            panic!("IPC command \"{command}\" was never called (expected args: {expected_args})");
603        }
604    }
605}
606
607/// Assert that a specific IPC command was NOT called.
608///
609/// # Panics
610///
611/// Panics if the command was found in the IPC log.
612///
613/// # Examples
614///
615/// ```
616/// use serde_json::json;
617///
618/// let log = json!([{"command": "save_settings", "args": {}}]);
619/// victauri_test::assert_ipc_not_called(&log, "delete_account");
620/// ```
621pub fn assert_ipc_not_called(log: &Value, command: &str) {
622    assert!(
623        !ipc_log_contains_command(log, command),
624        "expected IPC command \"{command}\" to NOT have been called, but it was"
625    );
626}
627
628#[cfg(test)]
629mod tests {
630    use serde_json::json;
631
632    use super::*;
633
634    #[test]
635    fn ipc_contains_finds_command_in_array() {
636        let log = json!([
637            {"command": "greet", "args": {"name": "World"}},
638            {"command": "save_settings", "args": {"theme": "dark"}}
639        ]);
640        assert!(ipc_log_contains_command(&log, "greet"));
641        assert!(ipc_log_contains_command(&log, "save_settings"));
642        assert!(!ipc_log_contains_command(&log, "delete_account"));
643    }
644
645    #[test]
646    fn ipc_contains_finds_command_in_entries_object() {
647        let log = json!({"entries": [{"command": "fetch_data"}]});
648        assert!(ipc_log_contains_command(&log, "fetch_data"));
649        assert!(!ipc_log_contains_command(&log, "nope"));
650    }
651
652    #[test]
653    fn args_match_partial_object() {
654        let actual = json!({"theme": "dark", "lang": "en", "notifications": true});
655        let expected = json!({"theme": "dark"});
656        assert!(args_match(&actual, &expected));
657    }
658
659    #[test]
660    fn args_match_full_object() {
661        let actual = json!({"theme": "dark"});
662        let expected = json!({"theme": "dark"});
663        assert!(args_match(&actual, &expected));
664    }
665
666    #[test]
667    fn args_match_fails_on_mismatch() {
668        let actual = json!({"theme": "light"});
669        let expected = json!({"theme": "dark"});
670        assert!(!args_match(&actual, &expected));
671    }
672
673    #[test]
674    fn args_match_scalar() {
675        assert!(args_match(&json!("hello"), &json!("hello")));
676        assert!(!args_match(&json!("hello"), &json!("world")));
677    }
678
679    #[test]
680    fn ipc_find_with_args_partial_match() {
681        let log = json!([
682            {"command": "save", "args": {"theme": "dark", "lang": "en"}}
683        ]);
684        let (found, _) = ipc_log_find_with_args(&log, "save", &json!({"theme": "dark"}));
685        assert!(found);
686    }
687
688    #[test]
689    fn ipc_find_with_args_no_match_returns_actual() {
690        let log = json!([
691            {"command": "save", "args": {"theme": "light"}}
692        ]);
693        let (found, actual) = ipc_log_find_with_args(&log, "save", &json!({"theme": "dark"}));
694        assert!(!found);
695        assert_eq!(actual, Some(json!({"theme": "light"})));
696    }
697
698    #[test]
699    fn ipc_find_with_args_command_not_found() {
700        let log = json!([{"command": "other", "args": {}}]);
701        let (found, actual) = ipc_log_find_with_args(&log, "save", &json!({"theme": "dark"}));
702        assert!(!found);
703        assert_eq!(actual, None);
704    }
705
706    #[test]
707    fn network_log_matches_url() {
708        let log = json!([
709            {"url": "http://api.example.com/users", "method": "GET", "status": 200},
710            {"url": "http://api.example.com/settings", "method": "POST", "status": 201}
711        ]);
712        assert!(network_log_matches(&log, None, "/users"));
713        assert!(network_log_matches(&log, Some("POST"), "/settings"));
714        assert!(!network_log_matches(&log, Some("DELETE"), "/settings"));
715        assert!(!network_log_matches(&log, None, "/nonexistent"));
716    }
717
718    #[test]
719    fn console_errors_filters_by_level() {
720        let log = json!([
721            {"level": "log", "message": "info msg"},
722            {"level": "error", "message": "something broke"},
723            {"level": "warn", "message": "careful"},
724            {"level": "error", "message": "another error"}
725        ]);
726        let errors = console_log_errors(&log);
727        assert_eq!(errors.len(), 2);
728        assert_eq!(errors[0], "something broke");
729        assert_eq!(errors[1], "another error");
730    }
731
732    #[test]
733    fn console_errors_empty_for_no_errors() {
734        let log = json!([{"level": "log", "message": "all good"}]);
735        assert!(console_log_errors(&log).is_empty());
736    }
737
738    #[test]
739    fn assert_ipc_called_passes() {
740        let log = json!([{"command": "greet", "args": {"name": "World"}}]);
741        assert_ipc_called(&log, "greet");
742    }
743
744    #[test]
745    #[should_panic(expected = "was not found in the log")]
746    fn assert_ipc_called_fails() {
747        let log = json!([{"command": "greet", "args": {}}]);
748        assert_ipc_called(&log, "nonexistent");
749    }
750
751    #[test]
752    fn assert_ipc_called_with_passes() {
753        let log = json!([{"command": "save", "args": {"theme": "dark", "extra": true}}]);
754        assert_ipc_called_with(&log, "save", &json!({"theme": "dark"}));
755    }
756
757    #[test]
758    #[should_panic(expected = "different args")]
759    fn assert_ipc_called_with_fails_wrong_args() {
760        let log = json!([{"command": "save", "args": {"theme": "light"}}]);
761        assert_ipc_called_with(&log, "save", &json!({"theme": "dark"}));
762    }
763
764    #[test]
765    fn assert_ipc_not_called_passes() {
766        let log = json!([{"command": "greet", "args": {}}]);
767        assert_ipc_not_called(&log, "delete_everything");
768    }
769
770    #[test]
771    #[should_panic(expected = "NOT have been called")]
772    fn assert_ipc_not_called_fails() {
773        let log = json!([{"command": "greet", "args": {}}]);
774        assert_ipc_not_called(&log, "greet");
775    }
776
777    #[test]
778    fn verify_report_all_passed() {
779        let report = VerifyReport {
780            results: vec![
781                CheckResult {
782                    description: "check1".into(),
783                    passed: true,
784                    detail: String::new(),
785                },
786                CheckResult {
787                    description: "check2".into(),
788                    passed: true,
789                    detail: String::new(),
790                },
791            ],
792        };
793        assert!(report.all_passed());
794        assert!(report.failures().is_empty());
795    }
796
797    #[test]
798    fn verify_report_with_failures() {
799        let report = VerifyReport {
800            results: vec![
801                CheckResult {
802                    description: "pass".into(),
803                    passed: true,
804                    detail: String::new(),
805                },
806                CheckResult {
807                    description: "fail".into(),
808                    passed: false,
809                    detail: "something wrong".into(),
810                },
811            ],
812        };
813        assert!(!report.all_passed());
814        assert_eq!(report.failures().len(), 1);
815        assert_eq!(report.failures()[0].description, "fail");
816    }
817
818    #[test]
819    #[should_panic(expected = "verify() failed")]
820    fn verify_report_assert_panics_on_failure() {
821        let report = VerifyReport {
822            results: vec![CheckResult {
823                description: "bad".into(),
824                passed: false,
825                detail: "it broke".into(),
826            }],
827        };
828        report.assert_all_passed();
829    }
830}