//! 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 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, &'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) -> Entity { 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, &'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::>()) }); 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::>()); 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 = ::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())); }