From 4e0f8ced0c81a5e0050a697fd158bb776a649dc7 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 7 Oct 2026 06:36:17 -0400 Subject: [PATCH 1/3] Tests: the hover-preview tests wait for the hover render, not just a quiet spell profile_browser_previews_on_hover_and_applies_on_click failed on FreeBSD CI (loupe showed "render", not "hover"): settle() returns after 12 idle frames, which can come before the hover render lands on a loaded VM. A step_until helper waits for the condition itself; the preset hover test had the same race. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/headless.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index a6b8fc432..4c7115de3 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -204,6 +204,20 @@ impl Headless { } } + /// Run frames until `done` holds or `timeout` passes (a render that finishes on a busy machine + /// after a quiet spell, where [`Self::settle`] alone would stop too early). Returns `done`. + pub fn step_until(&mut self, timeout: Duration, done: impl Fn(&Self) -> bool) -> bool { + let t0 = Instant::now(); + while !done(self) { + if t0.elapsed() > timeout { + return false; + } + self.step(); + std::thread::sleep(Duration::from_millis(1)); + } + true + } + /// Rasterize the last frame (with the photo textures). pub fn paint(&self) -> ColorImage { self.view.paint(&HashMap::new()) @@ -373,7 +387,8 @@ mod tests { let r = h.request("ui.hoverWidget", json!({"id": "profileCell:lc.vivid"}), t); assert_eq!(r["ok"], true, "{r}"); h.settle(SETTLE); - h.step(); + // the hover render can land after a quiet spell on a loaded machine (FreeBSD CI): wait for it + h.step_until(SETTLE, |h| h.app.loupe_shown.map(|l| l.1) == Some("hover")); assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Profile: Vivid")); assert_eq!(h.app.loupe_shown.map(|l| l.1), Some("hover")); let hover = h.app.renderer.textures.get(&crate::render::Slot::Hover).expect("hover render"); @@ -409,7 +424,7 @@ mod tests { let before = photo(&h); h.request("ui.hoverWidget", json!({"id": "preset:lc.bw-high-contrast"}), t); h.settle(SETTLE); - h.step(); + h.step_until(SETTLE, |h| h.app.loupe_shown.map(|l| l.1) == Some("hover")); assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: High Contrast B&W")); assert_eq!(h.app.loupe_shown.map(|l| l.1), Some("hover")); let after = photo(&h); From 517c745fad7b0f425fe59d51f1dcff0182404ce7 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 7 Oct 2026 10:18:53 -0400 Subject: [PATCH 2/3] Tests: the folder-tree test waits for the subfolder row, not three frames local_folder_tree_expands_and_browses failed on FreeBSD CI (707138f) and under local load: the expanded folder's listing can land after the fixed three frames. It now steps until the row is laid out (bounded by SETTLE). Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/tests_masking.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/tests_masking.rs b/crates/ui-egui/src/tests_masking.rs index 2abc141f3..9c8ea349e 100644 --- a/crates/ui-egui/src/tests_masking.rs +++ b/crates/ui-egui/src/tests_masking.rs @@ -371,10 +371,10 @@ fn local_folder_tree_expands_and_browses() { let base_s = base.to_string_lossy().to_string(); let r = h.request("ui.clickWidget", json!({"id": format!("folderToggle:{base_s}")}), T); assert_eq!(r["ok"], true, "{r}"); - for _ in 0..3 { - h.step(); - } let trip = base.join("Trip").to_string_lossy().to_string(); + // the folder listing can land a few frames later on a loaded machine (FreeBSD CI): wait for the row + let row = format!("source:local:{trip}"); + h.step_until(SETTLE, |h| h.app.widgets.iter().any(|(w, _)| *w == row)); let r = h.request("ui.clickWidget", json!({"id": format!("source:local:{trip}")}), T); assert_eq!(r["ok"], true, "the subfolder is listed: {r}"); assert_eq!(h.app.session.browse.as_ref().map(|b| b.path.clone()), Some(trip.clone()), "clicking it browses it"); From 6408ccf2442c233a67035ab557a83dff7a46f0c5 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 7 Oct 2026 10:58:29 -0400 Subject: [PATCH 3/3] Tests: the keyword-list test lets the list settle before clicking rows keyword_list_filters_renames_and_suggests failed on FreeBSD CI (b35487a) and locally (2 of 13 runs under load): ui.clickWidget clicks last frame's rect, and right after opening the panel / expanding "travel" the rows could still move, so the click hit "sunrise" instead of "travel|italy". Settling first: 30 of 30 runs pass. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/headless.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index 4c7115de3..4b599d022 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -535,12 +535,16 @@ mod tests { ex(&mut h, "photo.setMeta", json!({"ids": [vis[0], vis[1]], "addKeywords": ["travel|italy"]})); ex(&mut h, "photo.setMeta", json!({"ids": [vis[2]], "addKeywords": ["travel|france"]})); h.request("ui.set", json!({"leftPanel": true}), t); + // clicks land on last frame's layout: let the keyword list settle first (on a loaded machine a + // row could still move, and the click then hit its neighbour, e.g. "sunrise") + h.settle(SETTLE); let r = h.request("ui.clickWidget", json!({"id": "source:keyword:travel"}), t); assert_eq!(r["ok"], true, "{r}"); assert_eq!(h.app.session.filter.keyword.as_deref(), Some("travel")); assert_eq!(h.app.session.visible_cloned().len(), 3); // open the level, filter by the child h.request("ui.clickWidget", json!({"id": "keywordToggle:travel"}), t); + h.settle(SETTLE); let r = h.request("ui.clickWidget", json!({"id": "source:keyword:travel|italy"}), t); assert_eq!(r["ok"], true, "{r}"); assert_eq!(h.app.session.visible_cloned().len(), 2);