Files
deckcraft/crates/ui-egui/src/sorter_tests.rs
T
Nikita RakitinandCursor afd058a8ff UI: sections in the Slide Sorter
The sorter draws a bar for each section. Add Section splits at the current
slide, double-click renames in place, the chevron collapses that section, and
dragging the bar moves the section and its slides together.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 00:50:34 +03:00

172 lines
6.5 KiB
Rust

//! Slide Sorter sections: add, rename, collapse, and dragging a whole section.
use deckcraft_engine::Session;
use egui::Pos2;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
use crate::{Services, SlideApp};
fn harness(with_sections: bool) -> Harness<'static, SlideApp> {
let mut s = Session::with_new();
for _ in 0..3 {
s.execute("slide.new", &json!({})).unwrap();
}
if with_sections {
s.execute("section.add", &json!({"name": "Alpha", "at": 0})).unwrap();
s.execute("section.add", &json!({"name": "Beta", "at": 2})).unwrap();
}
let mut app = SlideApp::new(s, Services::default());
app.ui.view = crate::ViewMode::Sorter;
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
h.run_steps(3);
h
}
fn section_names(h: &Harness<'_, SlideApp>) -> Vec<String> {
h.state().session.active().unwrap().doc.sections.iter().map(|s| s.name.clone()).collect()
}
fn section_slides(h: &Harness<'_, SlideApp>) -> Vec<Vec<u32>> {
h.state().session.active().unwrap().doc.sections.iter().map(|s| s.slides.iter().map(|id| id.0).collect()).collect()
}
fn double_click(h: &mut Harness<'_, SlideApp>, p: Pos2) {
h.hover_at(p);
h.run_steps(1);
for _ in 0..2 {
for pressed in [true, false] {
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
h.run_steps(1);
}
}
h.run_steps(2);
}
fn drag(h: &mut Harness<'_, SlideApp>, from: Pos2, to: Pos2) {
h.hover_at(from);
h.run_steps(1);
h.drag_at(from);
h.run_steps(1);
for i in 1..=6 {
let t = i as f32 / 6.0;
h.hover_at(from + (to - from) * t);
h.run_steps(1);
}
h.drop_at(to);
h.run_steps(3);
}
#[test]
fn drop_slot_and_move_dest() {
assert_eq!(super::drop_slot(&[10.0, 40.0, 70.0], 0.0), 0);
assert_eq!(super::drop_slot(&[10.0, 40.0, 70.0], 20.0), 1);
assert_eq!(super::drop_slot(&[10.0, 40.0, 70.0], 100.0), 3);
assert_eq!(super::move_dest(1, 1), None);
assert_eq!(super::move_dest(1, 2), None);
assert_eq!(super::move_dest(1, 0), Some(0));
assert_eq!(super::move_dest(0, 2), Some(1));
}
#[test]
fn add_section_splits_at_the_current_slide() {
let mut h = harness(false);
assert!(h.query_by_label("Slide 4").is_some());
assert!(section_names(&h).is_empty());
h.get_by_label("Add Section").click();
h.run_steps(3);
assert_eq!(section_names(&h), ["Default Section", "Untitled Section"]);
let slides = section_slides(&h);
assert_eq!(slides[0].len(), 3);
assert_eq!(slides[1].len(), 1);
assert!(h.query_by_label("Default Section").is_some());
assert!(h.query_by_label("Untitled Section").is_some());
assert!(h.query_by_label("Slide 1").is_some() && h.query_by_label("Slide 4").is_some());
}
#[test]
fn collapse_hides_that_sections_slides_and_expand_brings_them_back() {
let mut h = harness(true);
assert_eq!(section_names(&h), ["Alpha", "Beta"]);
assert_eq!(section_slides(&h).iter().map(|s| s.len()).collect::<Vec<_>>(), [2, 2]);
assert!(h.query_by_label("Slide 1").is_some() && h.query_by_label("Slide 4").is_some());
h.get_by_label("Collapse Beta").click();
h.run_steps(2);
assert!(h.query_by_label("Slide 1").is_some() && h.query_by_label("Slide 2").is_some());
assert!(h.query_by_label("Slide 3").is_none() && h.query_by_label("Slide 4").is_none());
assert!(h.query_by_label("Expand Beta").is_some());
h.get_by_label("Expand Beta").click();
h.run_steps(2);
assert!(h.query_by_label("Slide 3").is_some() && h.query_by_label("Slide 4").is_some());
}
#[test]
fn double_click_renames_and_switching_documents_drops_the_draft() {
let mut h = harness(true);
let beta = h.get_by_label("Beta").rect().center();
double_click(&mut h, beta);
assert!(h.state().sorter.renaming.is_some(), "double-click starts renaming");
h.event(egui::Event::Text("Gamma".into()));
h.run_steps(1);
h.key_press(egui::Key::Enter);
h.run_steps(3);
assert_eq!(section_names(&h), ["Alpha", "Gamma"]);
assert!(h.state().sorter.renaming.is_none());
// Past the double-click window, so the next two clicks count as a new double-click.
h.run_steps(25);
let gamma = h.get_by_label("Gamma").rect().center();
double_click(&mut h, gamma);
assert!(h.state().sorter.renaming.is_some());
h.event(egui::Event::Text("Nope".into()));
h.run_steps(1);
h.key_press(egui::Key::Escape);
h.run_steps(3);
assert_eq!(section_names(&h), ["Alpha", "Gamma"]);
assert!(h.query_by_label("Nope").is_none());
h.run_steps(25);
let gamma = h.get_by_label("Gamma").rect().center();
double_click(&mut h, gamma);
assert!(h.state().sorter.renaming.is_some());
h.event(egui::Event::Text("Draft".into()));
h.run_steps(1);
h.state_mut().run("file.new", json!({})).unwrap();
h.run_steps(2);
assert!(h.state().sorter.renaming.is_none(), "a different presentation drops the draft");
h.state_mut().session.set_active(0);
h.run_steps(2);
assert_eq!(section_names(&h), ["Alpha", "Gamma"]);
}
#[test]
fn dragging_a_section_moves_it_and_keeps_it_collapsed() {
let mut h = harness(true);
let before = section_slides(&h);
h.get_by_label("Collapse Beta").click();
h.run_steps(2);
let (alpha, beta) = (h.get_by_label("Alpha").rect(), h.get_by_label("Beta").rect());
assert!(beta.top() > alpha.bottom());
drag(&mut h, beta.center(), Pos2::new(alpha.center().x, alpha.top() - 4.0));
assert_eq!(section_names(&h), ["Beta", "Alpha"]);
let after = section_slides(&h);
assert_eq!(after[0], before[1]);
assert_eq!(after[1], before[0]);
let doc = &h.state().session.active().unwrap().doc;
let order: Vec<u32> = doc.slides.iter().map(|s| s.id.0).collect();
let expect: Vec<u32> = after.iter().flatten().copied().collect();
assert_eq!(order, expect, "slides follow the section order");
assert!(h.query_by_label("Expand Beta").is_some(), "Beta stays collapsed after the move");
assert!(h.query_by_label("Slide 1").is_none() && h.query_by_label("Slide 2").is_none());
assert!(h.query_by_label("Slide 3").is_some() && h.query_by_label("Slide 4").is_some());
let (beta, alpha) = (h.get_by_label("Beta").rect(), h.get_by_label("Alpha").rect());
assert!(beta.top() < alpha.top());
}