464 lines
17 KiB
Rust
464 lines
17 KiB
Rust
//! Tables, charts, notes and comments from the deck (C10.3).
|
|
|
|
use ely_gpui_component::forms::TextInput;
|
|
use ely_lok::show::Objects;
|
|
use ely_office::prelude::*;
|
|
use gpui::{AppContext as _, Entity, TestAppContext, Window};
|
|
|
|
use super::objects::{act, blank, objects};
|
|
use super::{settle, undo_steps, update};
|
|
use crate::deck::Deck;
|
|
use crate::dialog::Dialog;
|
|
|
|
/// Runs `f` on the deck inside its window.
|
|
pub(super) fn in_window<R>(
|
|
cx: &mut TestAppContext,
|
|
deck: &Entity<Deck>,
|
|
f: impl FnOnce(&mut Deck, &mut Window, &mut gpui::Context<Deck>) -> R,
|
|
) -> R {
|
|
let handle = cx
|
|
.update(|app| deck.read(app).core.window)
|
|
.expect("a window");
|
|
cx.update_window(handle, |_, window, app| {
|
|
deck.update(app, |deck, cx| f(deck, window, cx))
|
|
})
|
|
.expect("the window lives")
|
|
}
|
|
|
|
pub(super) fn type_in(cx: &mut TestAppContext, field: &Entity<TextInput>, text: &str) {
|
|
let text = text.to_string();
|
|
cx.update(|app| field.update(app, |field, cx| field.set_text(text, cx)));
|
|
}
|
|
|
|
fn has(cx: &TestAppContext, deck: &Entity<Deck>, done: impl Fn(&Objects) -> bool) {
|
|
settle(cx, |app| objects(app, deck).as_ref().is_some_and(&done));
|
|
}
|
|
|
|
#[test]
|
|
fn the_chart_dialog_inserts_edits_and_refuses_words() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let chart_open = |app: &gpui::App| matches!(deck.read(app).dialog, Some(Dialog::Chart(_)));
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_chart(cx));
|
|
settle(&cx, chart_open);
|
|
update(&mut cx, &deck, |deck, cx| deck.chart_asked(cx));
|
|
has(&cx, &deck, |o| {
|
|
o.selection.as_ref().is_some_and(|s| s.kind == "chart")
|
|
});
|
|
// Again, with the chart marked: it edits.
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_chart(cx));
|
|
settle(&cx, chart_open);
|
|
let cell = cx.update(|app| match &deck.read(app).dialog {
|
|
Some(Dialog::Chart(editing)) => {
|
|
assert!(editing.changing, "the marked chart opens for editing");
|
|
editing.table.as_ref().expect("a table").series[0].1[0].clone()
|
|
}
|
|
_ => unreachable!("a chart dialog"),
|
|
});
|
|
type_in(&mut cx, &cell, "lots");
|
|
update(&mut cx, &deck, |deck, cx| deck.chart_asked(cx));
|
|
let refused = cx.update(|app| match &deck.read(app).dialog {
|
|
Some(Dialog::Chart(editing)) => editing.error.clone(),
|
|
_ => None,
|
|
});
|
|
assert!(refused.is_some(), "a word in a cell keeps the dialog open");
|
|
let steps = undo_steps(&mut cx, &deck);
|
|
type_in(&mut cx, &cell, "9");
|
|
update(&mut cx, &deck, |deck, cx| deck.chart_asked(cx));
|
|
settle(&cx, |app| deck.read(app).dialog.is_none());
|
|
assert_eq!(undo_steps(&mut cx, &deck), steps + 1, "one step");
|
|
let value = cx.update(|app| {
|
|
let deck = deck.read(app);
|
|
let tab = deck.active_tab().expect("a tab");
|
|
let mut worker = deck.core.worker.lock().expect("the worker lock");
|
|
let read = worker
|
|
.values(&tab.doc, ely_lok::sheet::CHART)
|
|
.expect("reads");
|
|
ely_lok::show::slide_chart(&read)
|
|
.expect("parses")
|
|
.expect("a chart")
|
|
.data
|
|
.expect("a table")
|
|
.series[0]
|
|
.1[0]
|
|
});
|
|
assert_eq!(value, Some(9.0));
|
|
}
|
|
|
|
#[test]
|
|
fn the_table_dialog_inserts_a_table() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_table(cx));
|
|
update(&mut cx, &deck, |deck, cx| deck.table_asked(cx));
|
|
has(&cx, &deck, |o| {
|
|
o.objects.iter().any(|object| object.kind == "table")
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn an_open_dialog_is_not_pushed_aside() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_table(cx));
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_new(cx));
|
|
// The chart dialog opens after a background read.
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_chart(cx));
|
|
cx.run_until_parked();
|
|
let kept = cx.update(|app| matches!(deck.read(app).dialog, Some(crate::dialog::Dialog::Table { .. })));
|
|
assert!(kept, "the table dialog stays");
|
|
}
|
|
|
|
#[test]
|
|
fn a_comment_lands_on_the_shown_slide() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
in_window(&mut cx, &deck, |deck, window, cx| {
|
|
deck.ask_remark(None, window, cx)
|
|
});
|
|
let (tab, edit) = cx.update(|app| match &deck.read(app).dialog {
|
|
Some(Dialog::Remark { tab, edit, .. }) => (*tab, edit.clone()),
|
|
_ => unreachable!("a comment dialog"),
|
|
});
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.send_remark((tab, edit), " Too much text ", cx)
|
|
});
|
|
let remarks = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.map(|tab| tab.extra.remarks.clone())
|
|
.unwrap_or_default()
|
|
};
|
|
settle(&cx, |app| remarks(app).len() == 1);
|
|
let remark = cx.update(|app| remarks(app).remove(0));
|
|
assert_eq!((remark.part, remark.text.as_str()), (0, "Too much text"));
|
|
}
|
|
|
|
#[test]
|
|
fn notes_typed_before_a_slide_change_stay_on_their_slide() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let notes = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.and_then(|tab| tab.extra.notes.clone())
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
let tab = deck.core.active;
|
|
deck.run_in(tab, "slide.insert", &[], cx)
|
|
});
|
|
let shown =
|
|
|part: usize| move |app: &gpui::App| notes(app).is_some_and(|n| n.part == Some(part));
|
|
settle(&cx, shown(1));
|
|
let view = |deck: &mut Deck| deck.active_tab().expect("a tab").view.clone();
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
view(deck).update(cx, |view, _| view.show_part(0))
|
|
});
|
|
settle(&cx, shown(0));
|
|
let field = in_window(&mut cx, &deck, |deck, window, cx| {
|
|
deck.notes_input(window, cx)
|
|
});
|
|
type_in(&mut cx, &field.expect("the field"), "Only for the first");
|
|
// The switch is queued before the blur's commit.
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
view(deck).update(cx, |view, _| view.show_part(1));
|
|
deck.commit_notes(cx);
|
|
});
|
|
settle(&cx, |app| {
|
|
notes(app).is_some_and(|n| n.part == Some(1) && n.text.is_empty())
|
|
});
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
view(deck).update(cx, |view, _| view.show_part(0))
|
|
});
|
|
settle(&cx, |app| {
|
|
notes(app).is_some_and(|n| n.part == Some(0) && n.text == "Only for the first")
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_chart_read_that_lands_late_opens_nothing() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_chart(cx));
|
|
settle(&cx, |app| {
|
|
matches!(deck.read(app).dialog, Some(Dialog::Chart(_)))
|
|
});
|
|
update(&mut cx, &deck, |deck, cx| deck.chart_asked(cx));
|
|
has(&cx, &deck, |o| {
|
|
o.selection.as_ref().is_some_and(|s| s.kind == "chart")
|
|
});
|
|
let first = cx.update(|app| deck.read(app).core.active).expect("a tab");
|
|
update(&mut cx, &deck, |deck, cx| deck.new_blank(cx));
|
|
settle(&cx, |app| deck.read(app).core.tabs.len() == 2);
|
|
let second = cx.update(|app| deck.read(app).core.active).expect("a tab");
|
|
update(&mut cx, &deck, |deck, _| deck.core.active = Some(first));
|
|
// Another tab comes forward before the read comes back.
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.ask_chart(cx);
|
|
deck.core.active = Some(second);
|
|
});
|
|
for _ in 0..100 {
|
|
cx.executor()
|
|
.advance_clock(std::time::Duration::from_millis(20));
|
|
cx.run_until_parked();
|
|
}
|
|
assert!(
|
|
cx.update(|app| deck.read(app).dialog.is_none()),
|
|
"no dialog for a hidden tab"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_chart_read_never_shows_another_chart_on_the_same_frame() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let charts = |o: &Objects| -> Vec<usize> {
|
|
let at = o.objects.iter().enumerate();
|
|
at.filter(|(_, o)| o.kind == "chart")
|
|
.map(|(at, _)| at)
|
|
.collect()
|
|
};
|
|
let title = |app: &gpui::App| match &deck.read(app).dialog {
|
|
Some(Dialog::Chart(editing)) => Some(editing.fields.title.read(app).text().to_string()),
|
|
_ => None,
|
|
};
|
|
for (n, name) in [(1, "First"), (2, "Second")] {
|
|
act(&mut cx, &deck, ("object.deselect", &["false"]), |o| {
|
|
o.selection.is_none()
|
|
});
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_chart(cx));
|
|
settle(&cx, |app| title(app).is_some());
|
|
let field = cx.update(|app| match &deck.read(app).dialog {
|
|
Some(Dialog::Chart(editing)) => editing.fields.title.clone(),
|
|
_ => unreachable!("a chart dialog"),
|
|
});
|
|
type_in(&mut cx, &field, name);
|
|
update(&mut cx, &deck, |deck, cx| deck.chart_asked(cx));
|
|
has(&cx, &deck, |o| charts(o).len() == n);
|
|
}
|
|
let both = cx.update(|app| charts(&objects(app, &deck).expect("objects")));
|
|
let first = both[0].to_string();
|
|
act(&mut cx, &deck, ("object.select", &[&first, "false"]), |o| {
|
|
o.objects[both[0]].selected
|
|
});
|
|
// The kernel picks the other chart before the read runs.
|
|
let (worker, doc) = cx.update(|app| {
|
|
let deck = deck.read(app);
|
|
(
|
|
deck.core.worker.clone(),
|
|
deck.active_tab().expect("a tab").doc.clone(),
|
|
)
|
|
});
|
|
let mut held = worker.lock().expect("the worker lock");
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_chart(cx));
|
|
let second = both[1].to_string();
|
|
let doc = held.now(&doc).expect("the state");
|
|
held.run(&doc, "object.select", &[&second, "false"])
|
|
.expect("selects");
|
|
drop(held);
|
|
for _ in 0..100 {
|
|
cx.executor()
|
|
.advance_clock(std::time::Duration::from_millis(20));
|
|
cx.run_until_parked();
|
|
}
|
|
let shown = cx.update(|app| title(app));
|
|
assert!(
|
|
shown.is_none() || shown.as_deref() == Some("First"),
|
|
"{shown:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_scatter_with_words_for_x_keeps_its_draft_and_names_the_cell() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| deck.ask_chart(cx));
|
|
let open = |app: &gpui::App| matches!(deck.read(app).dialog, Some(Dialog::Chart(_)));
|
|
settle(&cx, open);
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
if let Some(Dialog::Chart(editing)) = &mut deck.dialog {
|
|
editing.look.kind = "scatter".into();
|
|
}
|
|
deck.chart_asked(cx);
|
|
});
|
|
let error = cx.update(|app| match &deck.read(app).dialog {
|
|
Some(Dialog::Chart(editing)) => editing.error.clone(),
|
|
_ => None,
|
|
});
|
|
let first = cx.update(|app| Deck::text(app, "show.chart.category", &[("n", "1")]));
|
|
assert!(
|
|
error.is_some_and(|e| e.contains(first.as_ref())),
|
|
"the dialog stays and names it"
|
|
);
|
|
let fields = cx.update(|app| match &deck.read(app).dialog {
|
|
Some(Dialog::Chart(editing)) => editing.table.as_ref().expect("a table").categories.clone(),
|
|
_ => unreachable!("a chart dialog"),
|
|
});
|
|
for (at, field) in fields.iter().enumerate() {
|
|
type_in(&mut cx, field, &(10 * (at + 1)).to_string());
|
|
}
|
|
update(&mut cx, &deck, |deck, cx| deck.chart_asked(cx));
|
|
has(&cx, &deck, |o| {
|
|
o.selection.as_ref().is_some_and(|s| s.kind == "chart")
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_comment_picked_on_another_slide_shows_its_slide_again() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let tab = cx.update(|app| deck.read(app).core.active).expect("a tab");
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.send_remark((tab, None), "Here", cx)
|
|
});
|
|
let remark = |app: &gpui::App| {
|
|
let tab = deck.read(app).active_tab()?;
|
|
tab.extra.remarks.first().map(|remark| remark.id.clone())
|
|
};
|
|
settle(&cx, |app| remark(app).is_some());
|
|
let key: gpui::SharedString = cx.update(|app| remark(app)).expect("a comment").into();
|
|
let current = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.map(|tab| tab.extra.slides.current)
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| deck.pick_remark(&key, cx));
|
|
assert_eq!(
|
|
cx.update(|app| deck.read(app).shown_remark()),
|
|
Some(key.clone())
|
|
);
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.run_in(Some(tab), "slide.insert", &[], cx)
|
|
});
|
|
settle(&cx, |app| current(app) == Some(1));
|
|
assert_eq!(
|
|
cx.update(|app| deck.read(app).shown_remark()),
|
|
None,
|
|
"pickable again"
|
|
);
|
|
update(&mut cx, &deck, |deck, cx| deck.pick_remark(&key, cx));
|
|
settle(&cx, |app| current(app) == Some(0));
|
|
assert_eq!(cx.update(|app| deck.read(app).shown_remark()), Some(key));
|
|
}
|
|
|
|
#[test]
|
|
fn a_zoom_stays_through_later_reports() {
|
|
use ely_gpui_component::shell::ZoomStep;
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let view = |app: &gpui::App| deck.read(app).active_tab().expect("a tab").view.clone();
|
|
settle(&cx, |app| view(app).read(app).fitting());
|
|
update(&mut cx, &deck, |deck, cx| deck.zoom(ZoomStep::In, cx));
|
|
let zoom = cx.update(|app| view(app).read(app).zoom());
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
let (id, view) = deck.active_tab().map(|tab| (tab.id, tab.view.clone())).expect("a tab");
|
|
deck.observed(id, &view, cx);
|
|
});
|
|
cx.update(|app| {
|
|
let view = view(app);
|
|
assert!(!view.read(app).fitting(), "a report does not fit the slide again");
|
|
assert_eq!(view.read(app).zoom(), zoom, "the zoom stays");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_check_row_does_not_jump_among_masters() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| deck.masters(true, cx));
|
|
settle(&cx, |app| deck.read(app).active_tab().is_some_and(|tab| tab.extra.masters));
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.dialog = Some(crate::dialog::Dialog::Check(Vec::new()));
|
|
deck.go_to_finding(0, cx);
|
|
});
|
|
assert!(cx.update(|app| deck.read(app).dialog.is_some()), "the check stays open");
|
|
}
|
|
|
|
#[test]
|
|
fn a_refused_size_does_not_stay_shown() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let before = cx.update(|app| deck.read(app).active_tab().expect("a tab").extra.size);
|
|
update(&mut cx, &deck, |deck, cx| deck.resize((-500, -500), cx));
|
|
settle(&cx, |app| {
|
|
deck.read(app).active_tab().is_some_and(|tab| tab.extra.size_asked.is_none())
|
|
});
|
|
let extra = cx.update(|app| {
|
|
let tab = deck.read(app).active_tab().expect("a tab");
|
|
(tab.extra.size, tab.extra.shown_size())
|
|
});
|
|
assert_eq!(extra.0, before, "the kernel kept its size");
|
|
assert_eq!(extra.1, crate::design::mm100(before), "the fields show it");
|
|
}
|
|
|
|
#[test]
|
|
fn a_size_the_kernel_does_not_take_clears_after_its_read() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.resize((30_000, 19_050), cx);
|
|
// As if the kernel refused: it keeps another size.
|
|
let id = deck.core.active.expect("a tab");
|
|
let tab = deck.tab_mut(id).expect("a tab");
|
|
let (_, seq) = tab.extra.size_asked.expect("asked");
|
|
tab.extra.size_asked = Some(((40_000, 19_050), seq));
|
|
});
|
|
settle(&cx, |app| {
|
|
deck.read(app).active_tab().is_some_and(|tab| tab.extra.size_asked.is_none())
|
|
});
|
|
let read = cx.update(|app| crate::design::mm100(deck.read(app).active_tab().expect("a tab").extra.size));
|
|
assert!((read.0 - 30_000).abs() <= 50, "the kernel's own size shows: {read:?}");
|
|
}
|