test(ui): cover the dialog, the note's tooltip, Reinstall and stacked rows strictly

- The dialog's Install is disabled with a reason for a refused version.
- The out-of-reach tooltip in card and table; no note for a repository
  copy; the note for a release without a download here.
- Clicking Reinstall installs; clicking a greyed-out Install version X
  sends nothing.
- German rows: a single main button must sit on the menu's line, a
  stacked pair must be centred on it and stay out of the next row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
arnav-hiwarkarandClaude Opus 5.5 committed 2026-11-05 20:27:09 +05:30
1 parent 0fba50e46b
commit 7c09fb79f4
2 files changed
+102 -23

No files matched your search

+30 -18
View File
@@ -194,18 +194,9 @@ fn german_switches_persists_and_keeps_install_actions() {
let slot = retry.query_all_by_label("Installieren").next().unwrap().rect().width();
for app in ["PhotoCraft", "VectorCraft"] {
let menu = retry.get_by_label(&format!("Weitere Aktionen für {app}")).rect();
// The main button is the one ending just left of the menu (or overlapping it); a stacked
// Open and Update sit half a button above and below the menu's middle.
let main = retry
.query_all_by_role(egui::accesskit::Role::Button)
.filter(|n| {
(n.rect().center().y - menu.center().y).abs() < STACKED_OFFSET
&& n.rect().left() < menu.left()
&& n.rect().right() > menu.left() - 20.0
})
.find(|n| n.rect().height() < 40.0)
.unwrap();
assert!(main.rect().width() <= slot + 0.5, "{app}: {:?} wider than Installieren ({slot})", main.rect());
let main = main_button_in_line(&retry, menu, app);
assert!(main.right() > menu.left() - 20.0, "{app}: {main:?} isn't beside the menu {menu:?}");
assert!(main.width() <= slot + 0.5, "{app}: {main:?} wider than Installieren ({slot})");
}
let (mut web, _) = harness_language(ViewMode::Grid, egui::Theme::Dark, false, &restored.language, Vec::new());
web.run_steps(6);
@@ -244,6 +235,14 @@ fn a_table_update_too_narrow_to_sit_side_by_side_stacks() {
// Up-to-date LightCraft keeps a one-line row.
let light = h.get_by_label("Weitere Aktionen für LightCraft").rect();
assert!(update.bottom() - open.top() > light.height() * 1.5, "PhotoCraft's row grew for two buttons");
// The pair stays inside its own row: the next row's controls start below it.
let next = h
.query_all_by_label_contains("Weitere Aktionen für")
.map(|n| n.rect())
.filter(|r| r.top() > menu.top())
.min_by(|a, b| a.top().total_cmp(&b.top()))
.expect("a row below PhotoCraft's");
assert!(update.bottom() < next.top(), "Update {update:?} runs into the next row {next:?}");
sent.borrow_mut().clear();
h.get_by_label(keep).click();
@@ -257,6 +256,24 @@ fn a_table_update_too_narrow_to_sit_side_by_side_stacks() {
/// half a button (and half the seam) above and below it. Rows are further apart than this.
const STACKED_OFFSET: f32 = 20.0;
/// The main button of the row whose `⋯` menu is `menu` (the one ending nearest the menu), after
/// checking it is in line with the menu: centred on it, or, stacked over its twin (the button of
/// the same width ending at the same place), the pair centred on it. Only a stacked pair may sit
/// off the menu's middle.
fn main_button_in_line(h: &Harness<'static, Option<LauncherUi>>, menu: egui::Rect, what: &str) -> egui::Rect {
let near: Vec<egui::Rect> = h
.query_all_by_role(egui::accesskit::Role::Button)
.map(|n| n.rect())
.filter(|r| (r.center().y - menu.center().y).abs() < STACKED_OFFSET && r.left() < menu.left() && r.height() < 40.0)
.collect();
let main = *near.iter().max_by(|a, b| a.right().total_cmp(&b.right())).unwrap_or_else(|| panic!("{what}: no main button by {menu:?}"));
let column: Vec<&egui::Rect> = near.iter().filter(|r| (r.right() - main.right()).abs() < 0.5 && (r.left() - main.left()).abs() < 0.5).collect();
let (top, bottom) = column.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(t, b), r| (t.min(r.top()), b.max(r.bottom())));
let middle = (top + bottom) / 2.0;
assert!((middle - menu.center().y).abs() < 1.0, "{what}: {column:?} not in line with the menu {menu:?}");
main
}
/// At the launcher's narrowest, no main button outgrows its slot or crowds out the `⋯` menu (#956):
/// a kept update, a kept install and a plain update, in both views, whichever label `language` has.
fn main_buttons_fit_the_narrowest_window(language: &str) {
@@ -269,12 +286,7 @@ fn main_buttons_fit_the_narrowest_window(language: &str) {
let slot = h.query_all_by_label(t("Install")).next().unwrap().rect().width();
for app in ["PhotoCraft", "VectorCraft", "EffectCraft"] {
let menu = h.get_by_label(&fmt(t("More actions for {name}"), &[("name", app)])).rect();
let main = h
.query_all_by_role(egui::accesskit::Role::Button)
.filter(|n| (n.rect().center().y - menu.center().y).abs() < STACKED_OFFSET && n.rect().left() < menu.left())
.max_by(|a, b| a.rect().right().total_cmp(&b.rect().right()))
.unwrap()
.rect();
let main = main_button_in_line(&h, menu, &format!("{language} {view:?} {app}"));
assert!(main.width() <= slot + 0.5, "{language} {view:?} {app}: {main:?} wider than Install ({slot})");
assert!(main.right() <= menu.left(), "{language} {view:?} {app}: {main:?} overlaps the menu {menu:?}");
// The table scrolls sideways, with a fixed slot; cards have to fit the window.
+72 -5
View File
@@ -571,6 +571,31 @@ fn choosing_an_app_version_turns_its_main_action_into_install() {
assert_eq!(h.query_all_by_label("Open").count(), opens + 1, "PhotoCraft shows Open");
}
#[test]
/// The version dialog's Install follows the same answer as the card's "Install version X": for a
/// copy the backend can't replace with the chosen version (installed outside the launcher, say),
/// it is greyed out, says why on hover, and clicking it sends no install.
fn the_version_dialog_wont_install_a_version_the_install_would_refuse() {
let pin = signed_photocraft("0.4.0");
let (mut h, sent, queue) = version_menu(pin.clone(), vec![signed_photocraft("0.5.0"), pin]);
queue.borrow_mut().push(Event::Installable { app: "photocraft".into(), installable: false });
h.query_all_by_role_and_label(egui::accesskit::Role::Button, "v0.4.0").next().unwrap().click();
h.run_steps(8);
sent.borrow_mut().clear();
let row = h.get_by_label("Close").rect().center().y;
let install = h
.get_all_by_label("Install version v0.4.0")
.find(|n| (n.rect().center().y - row).abs() < 4.0)
.expect("the dialog shows Install for the chosen version");
assert!(install.accesskit_node().is_disabled(), "the install would be refused");
install.hover();
h.run_steps(4);
assert!(h.query_all_by_label_contains("can't install version v0.4.0 over this copy").next().is_some(), "it says why");
h.get_all_by_label("Install version v0.4.0").find(|n| (n.rect().center().y - row).abs() < 4.0).unwrap().click();
h.run_steps(4);
assert!(!sent.borrow().iter().any(|c| matches!(c, Command::Install { .. })), "{:?}", sent.borrow());
}
#[test]
/// A custom-folder install opens a confirmation dialog with no command until Install; the
/// chosen root and per-app folder are persisted, Configure precedes Install, and Cancel keeps
@@ -1128,6 +1153,7 @@ fn a_newer_release_without_a_download_here_is_not_offered_as_an_update() {
assert!(h.query_by_label(KEEP_PHOTOCRAFT).is_none(), "no split without an update");
h.get_by_label("Update to 0.6.0");
h.get_by_label("Update 1 app");
h.get_by_label_contains("0.4.1 · latest 0.5.0");
// PhotoCraft's main button is a plain Open, which launches it.
// Cards share a row, so PhotoCraft's Open is the one right before its own menu.
let menu = h.get_by_label("More actions for PhotoCraft").rect();
@@ -1152,10 +1178,15 @@ fn the_backends_installability_answer_decides_an_update() {
assert!(h.query_by_label("Update to 0.5.0").is_none(), "the backend can't install it");
assert!(h.query_by_label(KEEP_PHOTOCRAFT).is_none());
h.get_by_label("Update 1 app");
// Still a sign the release is out: a quiet note beside the installed version.
h.get_by_label_contains("0.4.1 · latest 0.5.0");
// Still a sign the release is out: a quiet note beside the installed version, saying why.
h.get_by_label_contains("0.4.1 · latest 0.5.0").hover();
h.run_steps(4);
h.get_by_label_contains(OUT_OF_REACH_TIP);
}
/// The tooltip on the note beside a copy behind a release it can't take.
const OUT_OF_REACH_TIP: &str = "0.5.0 is out, but ArtCraft Launcher can't install it over this copy.";
#[test]
/// The note beside a copy behind a release it can't take shows in the table too.
fn the_table_notes_a_newer_release_that_cant_install_here() {
@@ -1163,7 +1194,33 @@ fn the_table_notes_a_newer_release_that_cant_install_here() {
ev.push(Event::Installable { app: "photocraft".into(), installable: false });
let (mut h, _) = harness_with(ev, ViewMode::Table, egui::Theme::Dark, true, 1.0);
h.run_steps(8);
h.get_by_label("0.4.1 · latest 0.5.0");
h.get_by_label("0.4.1 · latest 0.5.0").hover();
h.run_steps(4);
h.get_by_label_contains(OUT_OF_REACH_TIP);
}
#[test]
/// A copy its repository updates (a Flatpak from FlatPark) knows nothing of GitHub's latest, so a
/// newer release there gets no "can't install" note: its repository may well have it.
fn a_repository_copy_gets_no_out_of_reach_note() {
let mut ev = events();
ev.push(Event::Detected {
app: "lightcraft".into(),
installed: Some(Installed {
version: version::parse("0.3.0"),
source: InstallSource::Flatpak,
location: "ai.storyteller.lightcraft".into(),
package: None,
externally_managed: true,
origin: Some("flatpark".into()),
installation: Some("user".into()),
}),
});
ev.push(Event::Installable { app: "lightcraft".into(), installable: false });
let (mut h, _) = harness_with(ev, ViewMode::Table, egui::Theme::Dark, true, 1.0);
h.run_steps(8);
assert!(h.query_all_by_label("0.3.0").next().is_some(), "LightCraft's version shows");
assert!(h.query_by_label_contains("0.3.0 · latest").is_none(), "no note for a repository copy");
}
#[test]
@@ -1173,7 +1230,7 @@ fn the_table_notes_a_newer_release_that_cant_install_here() {
fn a_kept_version_gets_no_out_of_reach_note_and_says_why_it_cant_install() {
let mut ev = events();
ev.push(Event::Installable { app: "photocraft".into(), installable: false });
let (mut h, _) = harness_with(ev, ViewMode::Grid, egui::Theme::Dark, true, 1.0);
let (mut h, sent) = harness_with(ev, ViewMode::Grid, egui::Theme::Dark, true, 1.0);
h.run_steps(8);
h.get_by_label_contains("0.4.1 · latest 0.5.0");
h.state_mut()
@@ -1189,6 +1246,10 @@ fn a_kept_version_gets_no_out_of_reach_note_and_says_why_it_cant_install() {
install.hover();
h.run_steps(4);
h.get_by_label_contains("can't install version v0.5.0 over this copy");
sent.borrow_mut().clear();
h.get_by_label("Install version v0.5.0").click();
h.run_steps(2);
assert_eq!(launches_and_installs(&sent, "photocraft"), (0, 0), "a greyed-out Install sends nothing: {:?}", sent.borrow());
}
#[test]
@@ -1223,11 +1284,17 @@ fn reinstall_needs_an_install_that_would_go_ahead() {
for installable in [true, false] {
let mut ev = events();
ev.push(Event::Installable { app: "lightcraft".into(), installable });
let (mut h, _) = harness_with(ev, ViewMode::Grid, egui::Theme::Dark, true, 1.0);
let (mut h, sent) = harness_with(ev, ViewMode::Grid, egui::Theme::Dark, true, 1.0);
h.run_steps(8);
h.get_by_label("More actions for LightCraft").click();
h.run_steps(3);
assert_eq!(h.query_by_label("Reinstall").is_some(), installable, "installable: {installable}");
if installable {
sent.borrow_mut().clear();
h.get_by_label("Reinstall").click();
h.run_steps(2);
assert_eq!(launches_and_installs(&sent, "lightcraft"), (0, 1), "Reinstall installs: {:?}", sent.borrow());
}
}
}