From 752a2f1482969865228f60877316637d440d7a3b Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 2 Sep 2025 10:45:16 +0200 Subject: [PATCH] Improved StepUntil --- .../re_viewer/src/viewer_test_utils/mod.rs | 83 +++++++++++++++---- crates/viewer/re_viewer/tests/app_kittest.rs | 21 ++--- .../tests/re_integration.rs | 30 +++---- 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs b/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs index d7b244d692bb..d5fd5f99cacd 100644 --- a/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs +++ b/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs @@ -1,5 +1,7 @@ use egui_kittest::Harness; +use egui_kittest::kittest::{By, Queryable}; use re_build_info::build_info; +use std::sync::Arc; use crate::{ App, AppEnvironment, AsyncRuntimeHandle, MainThreadToken, StartupOptions, @@ -31,22 +33,73 @@ pub fn viewer_harness() -> Harness<'static, App> { }) } -/// Steps through the harness until the `predicate` closure returns `true`. -pub async fn step_until<'app, 'harness, Predicate>( - harness: &'harness mut egui_kittest::Harness<'app, App>, - mut predicate: Predicate, +/// Utility to wait until some widget appears. +pub struct StepUntil { step_duration: tokio::time::Duration, max_duration: tokio::time::Duration, -) where - Predicate: for<'a> FnMut(&'a egui_kittest::Harness<'app, App>) -> bool, -{ - let start_time = std::time::Instant::now(); - let mut success = predicate(harness); - while !success && start_time.elapsed() <= max_duration { - harness.step(); - tokio::time::sleep(step_duration).await; - harness.step(); - success = predicate(harness); + debug_label: String, +} + +impl StepUntil { + pub fn new(debug_label: impl ToString) -> Self { + Self { + debug_label: debug_label.to_string(), + step_duration: tokio::time::Duration::from_millis(100), + max_duration: tokio::time::Duration::from_secs(5), + } + } + + /// Step duration. + /// + /// Default is 100ms. + pub fn step_duration(mut self, duration: tokio::time::Duration) -> Self { + self.step_duration = duration; + self + } + + /// Max duration to wait for the predicate to be true. + /// + /// Default is 5 seconds. + pub fn max_duration(mut self, duration: tokio::time::Duration) -> Self { + self.max_duration = duration; + self + } + + /// Set the max_duration to step_duration * steps + pub fn steps(mut self, steps: u32) -> Self { + self.max_duration = self.step_duration * steps; + self + } + + /// Step until the predicate returns Some. + pub async fn run<'app, 'harness: 'pre, 'pre, State, Predicate, R>( + self, + harness: &'harness mut egui_kittest::Harness<'app, State>, + mut predicate: Predicate, + ) -> R + where + Predicate: FnMut(&'pre Harness<'app, State>) -> Option, + { + let start_time = std::time::Instant::now(); + loop { + match predicate(harness) { + Some(result) => { + return result; + } + None => { + if start_time.elapsed() > self.max_duration { + panic!( + r#"Timed out waiting for "{}" + +Found nodes: {:?}"#, + self.debug_label, + harness.root() + ); + } + tokio::time::sleep(self.step_duration).await; + harness.step(); + } + } + } } - assert!(success, "Timed out waiting for predicate to be true."); } diff --git a/crates/viewer/re_viewer/tests/app_kittest.rs b/crates/viewer/re_viewer/tests/app_kittest.rs index 151e41ded32c..8a638b542627 100644 --- a/crates/viewer/re_viewer/tests/app_kittest.rs +++ b/crates/viewer/re_viewer/tests/app_kittest.rs @@ -3,6 +3,7 @@ use egui_kittest::kittest::Queryable as _; use re_viewer::viewer_test_utils; +use re_viewer::viewer_test_utils::StepUntil; /// Navigates from welcome to settings screen and snapshots it. #[cfg(not(windows))] // TODO(#10971): Fix it @@ -13,18 +14,12 @@ async fn settings_screen() { harness.run_ok(); harness.get_by_label_contains("Settings…").click(); // Wait for the FFmpeg-check loading spinner to disappear. - viewer_test_utils::step_until( - &mut harness, - |harness| { - harness - .query_by_label_contains( - "The specified FFmpeg binary path does not exist or is not a file.", - ) - .is_some() - }, - tokio::time::Duration::from_millis(100), - tokio::time::Duration::from_secs(5), - ) - .await; + StepUntil::new("loading ffmpeg version") + .run(&mut harness, |harness| { + harness.query_by_label_contains( + "The specified FFmpeg binary path does not exist or is not a file.", + ) + }) + .await; harness.snapshot("settings_screen"); } diff --git a/tests/rust/re_integration_test/tests/re_integration.rs b/tests/rust/re_integration_test/tests/re_integration.rs index c289301ba1bb..fabbc9ac8f75 100644 --- a/tests/rust/re_integration_test/tests/re_integration.rs +++ b/tests/rust/re_integration_test/tests/re_integration.rs @@ -3,6 +3,7 @@ use egui_kittest::kittest::Queryable as _; use insta::with_settings; use re_integration_test::{TestServer, load_test_data}; use re_viewer::viewer_test_utils; +use re_viewer::viewer_test_utils::StepUntil; // #[test] // TODO(emilk): re-enable when they actually work pub fn integration_test() { @@ -48,25 +49,18 @@ pub async fn dataset_ui_test() { harness.get_by_label("Add").click(); harness.run_ok(); - viewer_test_utils::step_until( - &mut harness, - |harness| harness.query_by_label_contains("my_dataset").is_some(), - tokio::time::Duration::from_millis(100), - tokio::time::Duration::from_secs(5), - ) - .await; + StepUntil::new("Waiting for 'my_dataset' list item") + .run(&mut harness, |harness| { + harness.query_by_label_contains("my_dataset") + }) + .await; harness.get_by_label("my_dataset").click(); - viewer_test_utils::step_until( - &mut harness, - |harness| { - harness - .query_by_label_contains("new_recording_id") - .is_some() - }, - tokio::time::Duration::from_millis(100), - tokio::time::Duration::from_secs(5), - ) - .await; + StepUntil::new("Waiting for dataset table (new_recording_id) to load") + .run(&mut harness, |harness| { + harness.query_by_label_contains("new_recording_id") + }) + .await; + snapshot_results.add(harness.try_snapshot("dataset_ui_table")); }