mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-26 11:09:23 -06:00
Tests for the grid navigation keys, Cmd+O and Shift+H
Cover Home/End and Shift+Left/Right in both grids (ends of the view, the loupe, the user's own binding winning), Cmd+O asking for a folder once, Shift+H toggling the HDR view, and the new keys staying out of the way while a text field has the keyboard, while the full-screen preview covers the grid, and while a dialog is open.
This commit is contained in:
@@ -237,3 +237,236 @@ fn opening_numeric_release_keeps_keys_in_the_editor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Press `key` with the named modifiers held (`cmd`, `shift`, `alt`, `ctrl`).
|
||||
fn press(h: &mut Headless, key: &str, mods: &[&str]) {
|
||||
let mut p = json!({ "key": key });
|
||||
for m in mods {
|
||||
p[*m] = json!(true);
|
||||
}
|
||||
let r = h.request("ui.key", p, T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
}
|
||||
|
||||
fn view(h: &mut Headless, p: serde_json::Value) {
|
||||
let r = h.request("ui.set", p, T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
|
||||
/// Make `id` the only selected, active photo.
|
||||
fn select(h: &mut Headless, id: lightcraft_catalog::PhotoId) {
|
||||
let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0], "active": id.0}}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
|
||||
/// The demo library in Photo Grid with its middle photo selected: the photos in view, and that one.
|
||||
fn grid_demo() -> (Headless, Vec<lightcraft_catalog::PhotoId>, lightcraft_catalog::PhotoId) {
|
||||
let mut h = demo();
|
||||
view(&mut h, json!({ "view": "photoGrid" }));
|
||||
let vis = h.app.session.visible_cloned();
|
||||
assert!(vis.len() > 2, "{vis:?}");
|
||||
let middle = vis[vis.len() / 2];
|
||||
select(&mut h, middle);
|
||||
(h, vis, middle)
|
||||
}
|
||||
|
||||
/// Count how often the folder picker is asked for; every ask is cancelled.
|
||||
fn count_folder_picks(h: &mut Headless) -> std::sync::Arc<std::sync::atomic::AtomicUsize> {
|
||||
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let observed = calls.clone();
|
||||
h.app.services.pick_folder = Some(Box::new(move || {
|
||||
observed.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
None
|
||||
}));
|
||||
calls
|
||||
}
|
||||
|
||||
/// The declared keys of the two commands these tests bind.
|
||||
#[test]
|
||||
fn open_library_and_visualize_hdr_keys_are_declared() {
|
||||
use crate::shortcuts::{Keymap, parse, shortcut_of};
|
||||
|
||||
let keymap = Keymap::new();
|
||||
for (id, want) in [("view.visualizeHdr", "Shift+H"), ("app.openLibrary", "Cmd+O")] {
|
||||
assert_eq!(shortcut_of(&keymap, id), Some(want), "{id}");
|
||||
assert!(parse(want).is_some(), "{want} does not parse");
|
||||
}
|
||||
}
|
||||
|
||||
/// Opening a library uses the same folder picker as File > Open Library. Cancelling it
|
||||
/// leaves the current photo and library intact.
|
||||
#[test]
|
||||
fn cmd_o_opens_the_library_picker_once() {
|
||||
let mut h = demo();
|
||||
let calls = count_folder_picks(&mut h);
|
||||
let active = h.app.session.active();
|
||||
let photos = h.app.session.catalog.len();
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1, "one picker for Cmd+O");
|
||||
assert_eq!(h.app.session.active(), active);
|
||||
assert_eq!(h.app.session.catalog.len(), photos);
|
||||
}
|
||||
|
||||
/// Shift+H toggles View > Visualize HDR; Cmd+Shift+H (Histogram) is untouched.
|
||||
#[test]
|
||||
fn shift_h_toggles_the_hdr_view() {
|
||||
let mut h = demo();
|
||||
assert!(!h.app.ui.hdr_visualize);
|
||||
let histogram = h.app.ui.histogram;
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(h.app.ui.hdr_visualize, "Shift+H turns Visualize HDR on");
|
||||
assert_eq!(h.app.ui.histogram, histogram, "Shift+H leaves the Histogram unchanged");
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(!h.app.ui.hdr_visualize, "Shift+H turns it back off");
|
||||
}
|
||||
|
||||
/// Home / End make the first / last photo of the view the active one, only in the library grids,
|
||||
/// and yield to a key the user gave to another command.
|
||||
#[test]
|
||||
fn home_and_end_select_the_ends_of_the_grid() {
|
||||
let (mut h, vis, _) = grid_demo();
|
||||
let (first, last) = (vis[0], *vis.last().unwrap());
|
||||
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(first), "Home goes to the first photo");
|
||||
assert_eq!(h.app.session.selection.ids, vec![first], "and selects only it");
|
||||
press(&mut h, "End", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(last), "End goes to the last photo");
|
||||
assert_eq!(h.app.session.selection.ids, vec![last]);
|
||||
|
||||
view(&mut h, json!({ "view": "squareGrid" }));
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(first), "the Square Grid has the keys too");
|
||||
|
||||
select(&mut h, last);
|
||||
view(&mut h, json!({ "view": "detail" }));
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(last), "Home is a grid key");
|
||||
|
||||
view(&mut h, json!({ "view": "photoGrid" }));
|
||||
let r = h.app.run("app.setShortcut", json!({"id": "view.survey", "shortcut": "Home"})).unwrap();
|
||||
assert_eq!(r["shortcut"], "Home", "{r}");
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.ui.view, ViewMode::Survey, "the user's Home binding fires");
|
||||
assert_eq!(h.app.session.active(), Some(last), "the grid key stayed out of the way");
|
||||
}
|
||||
|
||||
/// Shift+Left / Shift+Right in a grid add the previous / next photo of the view to the selection and
|
||||
/// make it active, stop at either end, stay out of the loupe and yield to the user's keymap.
|
||||
#[test]
|
||||
fn shift_arrows_extend_the_grid_selection() {
|
||||
let (mut h, vis, middle) = grid_demo();
|
||||
let at = vis.iter().position(|p| *p == middle).unwrap();
|
||||
let (previous, next) = (vis[at - 1], vis[at + 1]);
|
||||
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle, next], "Shift+Right adds the next photo");
|
||||
assert_eq!(h.app.session.active(), Some(next), "the added photo becomes active");
|
||||
// Shift+Left steps back onto a photo that is already selected: the selection stays, the active moves
|
||||
press(&mut h, "Left", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle, next]);
|
||||
assert_eq!(h.app.session.active(), Some(middle));
|
||||
press(&mut h, "Left", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle, next, previous], "Shift+Left adds the previous photo");
|
||||
assert_eq!(h.app.session.active(), Some(previous));
|
||||
|
||||
// the ends of the view do nothing
|
||||
select(&mut h, vis[0]);
|
||||
press(&mut h, "Left", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![vis[0]], "Shift+Left on the first photo is a no-op");
|
||||
let last = *vis.last().unwrap();
|
||||
select(&mut h, last);
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![last], "Shift+Right on the last photo is a no-op");
|
||||
|
||||
// outside a grid it is not the extension key
|
||||
select(&mut h, middle);
|
||||
view(&mut h, json!({ "view": "detail" }));
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle], "the loupe has no grid extension key");
|
||||
|
||||
// a key the user gave to a command wins
|
||||
view(&mut h, json!({ "view": "photoGrid" }));
|
||||
h.app.run("app.setShortcut", json!({"id": "view.survey", "shortcut": "Shift+Right"})).unwrap();
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.ui.view, ViewMode::Survey, "the user's Shift+Right binding fires");
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle], "the grid key stayed out of the way");
|
||||
}
|
||||
|
||||
/// A text field with the keyboard focus keeps every one of these keys.
|
||||
#[test]
|
||||
fn navigation_open_library_and_hdr_keys_wait_for_a_text_field() {
|
||||
let (mut h, vis, middle) = grid_demo();
|
||||
let calls = count_folder_picks(&mut h);
|
||||
let picks = || calls.load(std::sync::atomic::Ordering::SeqCst);
|
||||
let keys: [(&str, &[&str]); 6] = [("Home", &[]), ("End", &[]), ("Right", &["shift"]), ("Left", &["shift"]), ("O", &["cmd"]), ("H", &["shift"])];
|
||||
|
||||
// with no text field focused, each of them acts
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(vis[0]));
|
||||
press(&mut h, "End", &[]);
|
||||
assert_eq!(h.app.session.active(), vis.last().copied());
|
||||
select(&mut h, middle);
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids.len(), 2, "Shift+Right extends");
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(picks(), 1, "Cmd+O asks for a folder");
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(h.app.ui.hdr_visualize, "Shift+H turns Visualize HDR on");
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(!h.app.ui.hdr_visualize);
|
||||
select(&mut h, middle);
|
||||
|
||||
// with the search box focused, none does
|
||||
let r = h.request("ui.clickWidget", json!({"id": "field:search"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(h.view.ctx.egui_wants_keyboard_input(), "the search box has the keyboard");
|
||||
let before = (h.app.session.active(), h.app.session.selection.ids.clone(), h.app.ui.hdr_visualize, h.app.ui.view, picks());
|
||||
for (key, mods) in keys {
|
||||
press(&mut h, key, mods);
|
||||
let after = (h.app.session.active(), h.app.session.selection.ids.clone(), h.app.ui.hdr_visualize, h.app.ui.view, picks());
|
||||
assert_eq!(after, before, "{mods:?}+{key} reached the shortcuts while the search box had the focus");
|
||||
}
|
||||
assert!(h.app.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
/// Home / End and Shift+Left / Shift+Right belong to the grid: while the full-screen preview covers it
|
||||
/// they leave the selection alone.
|
||||
#[test]
|
||||
fn grid_navigation_keys_do_nothing_in_the_full_screen_preview() {
|
||||
let (mut h, vis, middle) = grid_demo();
|
||||
h.app.run("view.fullScreenPreview", json!({})).unwrap();
|
||||
assert!(h.app.ui.fullscreen);
|
||||
assert_eq!(h.app.ui.view, ViewMode::PhotoGrid, "the grid is the view behind the preview");
|
||||
for (key, mods) in [("Home", &[][..]), ("End", &[][..]), ("Right", &["shift"][..]), ("Left", &["shift"][..])] {
|
||||
press(&mut h, key, mods);
|
||||
assert_eq!(h.app.session.active(), Some(middle), "{mods:?}+{key} moved the active photo in the full-screen preview");
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle], "{mods:?}+{key} changed the selection in the full-screen preview");
|
||||
}
|
||||
// back in the grid the keys work again
|
||||
h.app.run("view.fullScreenPreview", json!({})).unwrap();
|
||||
assert!(!h.app.ui.fullscreen);
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(vis[0]));
|
||||
}
|
||||
|
||||
/// A dialog owns the keyboard: Cmd+O does not ask for a folder behind it.
|
||||
#[test]
|
||||
fn cmd_o_does_not_fire_while_a_dialog_is_open() {
|
||||
let mut h = demo();
|
||||
let calls = count_folder_picks(&mut h);
|
||||
h.app.run("app.shortcuts", json!({})).unwrap();
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::Shortcuts)));
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 0, "Cmd+O reached the shortcuts behind a dialog");
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::Shortcuts)));
|
||||
// with the dialog closed the same key asks for a folder
|
||||
h.app.ui.dialog = None;
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user