202 lines
7.6 KiB
Rust
202 lines
7.6 KiB
Rust
//! The viewer against the real worker and generated PDFs (C11.1).
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use ely_office::{Opening as _, Tabs as _};
|
|
use ely_pdf_engine::{Worker, sample::pdf};
|
|
use gpui::{Entity, TestAppContext, VisualTestContext};
|
|
|
|
use crate::engine::Mupdf;
|
|
use crate::layout::Mode;
|
|
use crate::select::Selection;
|
|
use crate::view::PdfView;
|
|
use crate::viewer::Viewer;
|
|
|
|
mod editing;
|
|
mod input;
|
|
mod notes;
|
|
mod organize;
|
|
mod output;
|
|
mod protect;
|
|
mod reads;
|
|
mod removal;
|
|
mod sign;
|
|
mod structure;
|
|
mod view;
|
|
mod visible;
|
|
|
|
/// Steps and redraws until `done`, at most ten seconds.
|
|
pub(crate) fn settle(cx: &mut VisualTestContext, done: impl Fn(&gpui::App) -> bool) {
|
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
|
while std::time::Instant::now() < deadline {
|
|
// Bounded: a busy loop must fail here, not hang.
|
|
for _ in 0..100 {
|
|
if !cx.executor().tick() {
|
|
break;
|
|
}
|
|
}
|
|
cx.update(|window, _| window.refresh());
|
|
if cx.update(|_, app| done(app)) {
|
|
return;
|
|
}
|
|
}
|
|
panic!("the state never settled");
|
|
}
|
|
|
|
/// A few redraws with the work they start.
|
|
pub(crate) fn frames(cx: &mut VisualTestContext) {
|
|
for _ in 0..5 {
|
|
cx.update(|window, _| window.refresh());
|
|
for _ in 0..20 {
|
|
cx.executor().tick();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn file(dir: &std::path::Path, name: &str, pages: &[(f32, f32, &[&str])]) -> std::path::PathBuf {
|
|
let path = dir.join(name);
|
|
std::fs::write(&path, pdf(pages, &[])).expect("writes");
|
|
path
|
|
}
|
|
|
|
/// A viewer drawn in a window, `paths` opened in order.
|
|
pub(crate) fn windowed<'a>(
|
|
cx: &'a mut TestAppContext,
|
|
dir: &std::path::Path,
|
|
paths: &[std::path::PathBuf],
|
|
worker: bool,
|
|
) -> (Entity<Viewer>, &'a mut VisualTestContext) {
|
|
cx.update(ely_gpui_component::init_for_tests);
|
|
cx.update(|cx| {
|
|
ely_ui::settings::install_defaults(cx);
|
|
ely_ui::i18n::install(cx);
|
|
});
|
|
cx.update(crate::keys::bind);
|
|
let held = match worker {
|
|
true => Mupdf::with(Worker::spawn().expect("the worker")),
|
|
false => Mupdf::default(),
|
|
};
|
|
let work = dir.join("work");
|
|
std::fs::create_dir_all(&work).expect("a work dir");
|
|
let (viewer, vcx) = cx.add_window_view(move |window, cx| {
|
|
let mut viewer = Viewer::new(Arc::new(Mutex::new(held)), work, cx);
|
|
viewer.core.window = Some(window.window_handle());
|
|
viewer
|
|
});
|
|
for path in paths {
|
|
let count = vcx.update(|_, app| viewer.read(app).core.tabs.len());
|
|
vcx.update(|_, app| viewer.update(app, |viewer, cx| viewer.open_file(path.clone(), cx)));
|
|
settle(vcx, |app| viewer.read(app).core.tabs.len() == count + 1);
|
|
}
|
|
(viewer, vcx)
|
|
}
|
|
|
|
/// The active tab's view.
|
|
pub(crate) fn shown(cx: &mut VisualTestContext, viewer: &Entity<Viewer>) -> Entity<PdfView> {
|
|
cx.update(|_, app| viewer.read(app).active_view().expect("a tab"))
|
|
}
|
|
|
|
/// Three pages with "alpha" on one and three, and bookmarks.
|
|
fn opened<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext) {
|
|
let pages: &[(f32, f32, &[&str])] = &[
|
|
(612.0, 792.0, &["alpha one", "second line"]),
|
|
(612.0, 792.0, &["middle page"]),
|
|
(612.0, 792.0, &["alpha two alpha"]),
|
|
];
|
|
let path = dir.join("three.pdf");
|
|
std::fs::write(&path, pdf(pages, &[("Start", 0, &[("Inside", 1)]), ("End", 2, &[])])).expect("writes");
|
|
windowed(cx, dir, &[path], true)
|
|
}
|
|
|
|
#[test]
|
|
fn a_pdf_opens_with_its_pages_and_bookmarks() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let (viewer, cx) = opened(&mut cx, dir.path());
|
|
let view = shown(cx, &viewer);
|
|
let (sizes, marks) = cx.update(|_, app| {
|
|
let view = view.read(app);
|
|
(view.sizes.clone(), view.marks.iter().map(|m| (m.title.clone(), m.down.len())).collect::<Vec<_>>())
|
|
});
|
|
assert_eq!(sizes, [(612.0, 792.0); 3]);
|
|
assert_eq!(marks, [("Start".to_string(), 1), ("End".to_string(), 0)]);
|
|
}
|
|
|
|
#[test]
|
|
fn find_steps_through_every_hit_across_pages() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let (viewer, cx) = opened(&mut cx, dir.path());
|
|
let view = shown(cx, &viewer);
|
|
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.open_find(window, cx)));
|
|
cx.simulate_keystrokes("a l p h a");
|
|
settle(cx, |app| view.read(app).found.len() == 3 && viewer.read(app).find.as_ref().is_some_and(|find| find.done == 3));
|
|
let at = |cx: &mut VisualTestContext| cx.update(|_, app| {
|
|
let view = view.read(app);
|
|
(view.chosen, view.found[view.chosen.expect("chosen")].0)
|
|
});
|
|
assert_eq!(at(cx), (Some(0), 0), "the first hit is chosen");
|
|
cx.simulate_keystrokes("enter");
|
|
cx.simulate_keystrokes("enter");
|
|
assert_eq!(at(cx), (Some(2), 2), "on to page three");
|
|
cx.simulate_keystrokes("escape");
|
|
assert!(cx.update(|_, app| viewer.read(app).find.is_none()), "escape closes it");
|
|
assert!(cx.update(|_, app| view.read(app).found.is_empty()), "its marks go");
|
|
}
|
|
|
|
#[test]
|
|
fn a_selection_across_pages_copies_its_lines() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let (viewer, cx) = opened(&mut cx, dir.path());
|
|
let view = shown(cx, &viewer);
|
|
// Ends at real characters: page two's eighth, page one's seventh.
|
|
cx.update(|_, app| view.update(app, |view, cx| {
|
|
view.page_text(0, cx);
|
|
view.page_text(1, cx);
|
|
}));
|
|
settle(cx, |app| view.read(app).text.len() == 2);
|
|
let centre = |cx: &mut VisualTestContext, page: usize, at: usize| cx.update(|_, app| {
|
|
let r = view.read(app).text[&page][at].rect;
|
|
(page, ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0))
|
|
});
|
|
let (anchor, head) = (centre(cx, 1, 7), centre(cx, 0, 6));
|
|
cx.update(|_, app| view.update(app, |view, cx| {
|
|
view.selection = Some(Selection { anchor, head });
|
|
view.copy(cx);
|
|
}));
|
|
settle(cx, |app| app.read_from_clipboard().is_some());
|
|
let text = cx.update(|_, app| app.read_from_clipboard().and_then(|item| item.text()));
|
|
assert_eq!(text.as_deref(), Some("one\nsecond line\nmiddle p"), "both ends included");
|
|
}
|
|
|
|
#[test]
|
|
fn a_view_change_keeps_the_page_being_read() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let (viewer, cx) = opened(&mut cx, dir.path());
|
|
let view = shown(cx, &viewer);
|
|
cx.update(|_, app| view.update(app, |view, cx| view.show(2, 0.0, cx)));
|
|
settle(cx, |app| view.read(app).current == 2);
|
|
cx.update(|_, app| view.update(app, |view, cx| view.change(|view, _| view.mode = Mode::Single, cx)));
|
|
let pages = cx.update(|_, app| view.read(app).laid_out().placed.iter().map(|p| p.page).collect::<Vec<_>>());
|
|
assert_eq!(pages, [2], "single page shows the page read");
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_with_no_pages_is_refused_and_left_closed() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let (viewer, cx) = windowed(&mut cx, dir.path(), &[], true);
|
|
let path = file(dir.path(), "empty.pdf", &[]);
|
|
let worker = cx.update(|_, app| viewer.read(app).core.worker.clone());
|
|
let failed = <crate::engine::Pdf as ely_office::Engine>::load(
|
|
&mut worker.lock().expect("the lock"),
|
|
ely_office::Load::File(&path, ""),
|
|
);
|
|
assert_eq!(failed.err().map(|error| error.kind()), Some(std::io::ErrorKind::InvalidData));
|
|
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.open_file(path.clone(), cx)));
|
|
settle(cx, |app| viewer.read(app).core.loading.is_empty());
|
|
assert!(cx.update(|_, app| viewer.read(app).active_tab().is_none()));
|
|
}
|