UI tests: wait for folder listings and keep sidebar fixtures off Home

Folder listings arrive from background threads that Headless::busy() does
not track, so a fixed number of frames was not always enough under
full-suite load. Two tests failed intermittently and passed alone:
kept_local_root_stays_while_browsing_below_and_elsewhere and
local_folder_tree_expands_and_browses. They now wait for the expected
widget (wait_for_widget, test-only) and use their temporary folders as kept
roots. The kept-root test hides Home (in the test's in-memory session) so
the tree does not depend on the user's real folders.

local_location_can_be_hidden_and_restored now hides a kept root, reached
via "All Photographs". It no longer covers hiding a browsed temporary
folder: hiding a folder beneath Home expands the whole parent tree and
pushes the restore button out of view. That UI bug is not fixed here.

Fixture changes split from a327b98 (T0 thumbnail work), plus follow-ups.

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
radialmonster 2026-10-07 08:35:10 -04:00
parent 928c2c974f
commit f73aedcc94
2 changed files with 32 additions and 4 deletions

View File

@ -215,6 +215,20 @@ impl Headless {
self.paint()
}
/// Wait for a fixture widget whose background directory listing may still be loading.
#[cfg(test)]
pub(crate) fn wait_for_widget(&mut self, id: &str, timeout: Duration) -> bool {
let start = Instant::now();
while let Some(remaining) = timeout.checked_sub(start.elapsed()) {
let reply = self.request("ui.widgets", json!({"filter": id}), remaining);
if reply["result"].as_array().is_some_and(|widgets| widgets.iter().any(|w| w["id"].as_str() == Some(id))) {
return true;
}
std::thread::sleep(Duration::from_millis(1));
}
false
}
/// Send a control-protocol request (see [`crate::control`]) and run frames until it is
/// answered, then until its input has been consumed. Returns `{"ok": …, "result"|"error": …}`.
pub fn request(&mut self, method: &str, params: Value, timeout: Duration) -> Value {
@ -826,10 +840,18 @@ mod tests {
w["result"].as_array().unwrap().iter().filter_map(|x| x["id"].as_str().map(String::from)).collect()
};
h.request("ui.set", json!({"leftPanel": true}), t);
// Keep the fixture as a root rather than depending on its location
// relative to the machine's built-in Home folder.
h.request("engine.execute", json!({"command": "local.addRoot", "params": {"path": path}}), t);
h.request("engine.execute", json!({"command": "library.browse", "params": {"path": path}}), t);
h.settle(SETTLE);
assert!(ids(&mut h).contains(&format!("source:local:{path}")));
assert!(!ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"));
// Test sidebar visibility independently of browsing: hiding a folder
// beneath Home otherwise expands the user's entire parent tree and
// pushes the restore button outside the viewport.
assert_eq!(h.request("ui.clickWidget", json!({"id": "source:all"}), t)["ok"], true);
h.settle(SETTLE);
let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path}}), t);
assert_eq!(r["ok"], true, "{r}");
h.settle(SETTLE);
@ -869,11 +891,17 @@ mod tests {
w["result"].as_array().unwrap().iter().map(|x| (x["id"].as_str().unwrap().to_string(), x["rect"][0].as_f64().unwrap_or(0.0))).collect()
};
h.request("ui.set", json!({"leftPanel": true}), t);
// Home may also own this temporary directory. Keep the fixture tree
// independent of the user's real folders and their async listings.
if let Ok(home) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
assert_eq!(exec(&mut h, "local.hide", json!({"path": home}))["ok"], true);
}
assert_eq!(exec(&mut h, "local.addRoot", json!({"path": photos}))["ok"], true);
assert_eq!(exec(&mut h, "local.addRoot", json!({"path": format!("{photos}/")}))["result"]["roots"].as_array().map(Vec::len), Some(1));
exec(&mut h, "library.browse", json!({"path": day1}));
h.settle(SETTLE);
h.step();
assert!(h.wait_for_widget(&format!("source:local:{day2}"), t), "the sibling listing did not arrive");
let r = rects(&mut h);
assert!(r.contains_key(&format!("source:local:{photos}")), "the kept root stays: {r:?}");
assert!(r.contains_key(&format!("source:local:{day2}")), "the sibling stays reachable: {r:?}");
@ -883,9 +911,10 @@ mod tests {
h.settle(SETTLE);
assert_eq!(h.app.session.browse.as_ref().map(|b| b.path.clone()), Some(day2.clone()), "the sibling is browsed");
// another location: the kept root stays listed
exec(&mut h, "library.browse", json!({"path": other}));
assert_eq!(exec(&mut h, "library.browse", json!({"path": other}))["ok"], true);
h.settle(SETTLE);
h.step();
assert!(h.wait_for_widget(&format!("source:local:{other}"), t), "the other location did not arrive");
let r = rects(&mut h);
assert!(r.contains_key(&format!("source:local:{photos}")) && r.contains_key(&format!("source:local:{other}")), "{r:?}");
// restart: the kept roots come back with the saved UI state

View File

@ -366,15 +366,14 @@ fn local_folder_tree_expands_and_browses() {
std::fs::create_dir_all(base.join(".hidden")).unwrap();
let mut h = detail("panel.edit");
exec(&mut h, "view.leftPanel", json!({"show": true}));
exec(&mut h, "local.addRoot", json!({"path": base.to_string_lossy()}));
exec(&mut h, "library.browse", json!({"path": base.to_string_lossy()}));
h.settle(SETTLE);
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();
assert!(h.wait_for_widget(&format!("source:local:{trip}"), T), "the asynchronous subfolder listing did not arrive");
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");