Files
ElyOffice/apps/ely-show/src/tests/output.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

252 lines
10 KiB
Rust

//! PDF, pictures and handouts from the deck (C10.6).
use std::path::Path;
use ely_office::prelude::*;
use gpui::{AppContext as _, Entity, TestAppContext};
use super::objects::blank;
use super::play::three_slides;
use super::{settle, undo_steps, update};
use crate::deck::Deck;
use crate::output::{Kind, Output, Range};
fn output(cx: &mut TestAppContext, deck: &Entity<Deck>, kind: Kind, range: Range) -> Output {
cx.update(|app| {
let tab = deck.read(app).active_tab().expect("a tab");
Output {
tab: tab.id,
print: false,
kind,
range,
per: 2,
current: Some(1),
hidden: tab.extra.slides.list.iter().map(|slide| !slide.visible).collect(),
}
})
}
/// Pages of a PDF, by pdftotext's page breaks.
fn pages(pdf: &Path) -> Option<usize> {
let out = std::process::Command::new("pdftotext").arg(pdf).arg("-").output().ok()?;
let text = String::from_utf8_lossy(&out.stdout).into_owned();
Some(text.split('\x0c').count() - 1)
}
#[test]
fn slides_and_handouts_go_out_and_the_deck_stays_put() {
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;
};
three_slides(&mut cx, &deck);
let steps = undo_steps(&mut cx, &deck);
let (slides, handouts) = (dir.path().join("slides.pdf"), dir.path().join("handouts.pdf"));
let asked = output(&mut cx, &deck, Kind::Slides, Range::All);
update(&mut cx, &deck, |deck, cx| deck.export_pdf(&asked, slides.clone(), cx));
let asked = output(&mut cx, &deck, Kind::Handouts, Range::Slides(1, 2));
update(&mut cx, &deck, |deck, cx| deck.export_pdf(&asked, handouts.clone(), cx));
settle(&cx, |_| slides.exists() && handouts.exists());
assert_eq!(undo_steps(&mut cx, &deck), steps, "the deck is not touched");
let (Some(slides), Some(handouts)) = (pages(&slides), pages(&handouts)) else {
eprintln!("SKIP: no pdftotext to count pages");
return;
};
assert_eq!(slides, 2, "the hidden slide stays out");
assert_eq!(handouts, 1, "two slides on one handout page");
}
#[test]
fn pictures_are_one_png_a_shown_slide_and_never_overwrite() {
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;
};
three_slides(&mut cx, &deck);
let folder = dir.path().join("out");
std::fs::create_dir_all(&folder).expect("a folder");
let asked = output(&mut cx, &deck, Kind::Pictures, Range::All);
let into = (folder.clone(), "deck".to_string());
update(&mut cx, &deck, |deck, cx| deck.write_pictures(&asked, into, cx));
let written = |folder: &Path| {
let mut names: Vec<String> = std::fs::read_dir(folder)
.expect("reads")
.map(|entry| entry.expect("an entry").file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
};
settle(&cx, |_| written(&folder).len() == 2);
assert_eq!(written(&folder), ["deck-1.png", "deck-2.png"], "slide three is hidden");
let picture = image::open(folder.join("deck-1.png")).expect("a PNG");
assert_eq!(picture.width(), 1920);
let before = std::fs::read(folder.join("deck-1.png")).expect("reads");
let asked = output(&mut cx, &deck, Kind::Pictures, Range::Current);
let into = (folder.clone(), "deck".to_string());
update(&mut cx, &deck, |deck, cx| deck.write_pictures(&asked, into, cx));
for _ in 0..50 {
cx.run_until_parked();
}
assert_eq!(std::fs::read(folder.join("deck-1.png")).expect("reads"), before, "never overwritten");
}
#[test]
fn the_check_lists_font_link_and_object_with_their_slides() {
use crate::check::Problem;
use crate::dialog::Dialog;
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(deck) = super::deck(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/show/trouble.fodp");
let path = dir.path().join("trouble.fodp");
std::fs::copy(&fixture, &path).expect("copies");
update(&mut cx, &deck, |deck, cx| deck.open_file(path.clone(), cx));
settle(&cx, |app| {
let tab = deck.read(app).active_tab();
tab.is_some_and(|tab| !tab.view.read(app).missing_fonts().is_empty())
});
update(&mut cx, &deck, |deck, cx| deck.check_show(cx));
settle(&cx, |app| matches!(deck.read(app).dialog, Some(Dialog::Check(_))));
let found = cx.update(|app| match &deck.read(app).dialog {
Some(Dialog::Check(found)) => found.clone(),
_ => unreachable!("settled above"),
});
let short: Vec<(Problem, &str, Vec<usize>)> =
found.iter().map(|f| (f.problem, f.name.as_str(), f.slides.clone())).collect();
assert!(short.contains(&(Problem::Font, "Nowhere Sans", vec![0])), "{short:?}");
assert!(short.contains(&(Problem::Link, "file:///nowhere/gone.png", vec![1])), "{short:?}");
assert!(short.contains(&(Problem::Object, "Empty", vec![2])), "{short:?}");
}
#[cfg(unix)]
#[test]
fn pictures_claim_every_name_first_and_follow_no_link() {
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;
};
three_slides(&mut cx, &deck);
let folder = dir.path().join("out");
std::fs::create_dir_all(&folder).expect("a folder");
// The second name links to a file not there.
std::os::unix::fs::symlink("../unrelated.png", folder.join("deck-2.png")).expect("links");
let asked = output(&mut cx, &deck, Kind::Pictures, Range::All);
let into = (folder.clone(), "deck".to_string());
update(&mut cx, &deck, |deck, cx| deck.write_pictures(&asked, into, cx));
for _ in 0..200 {
cx.run_until_parked();
}
assert!(!dir.path().join("unrelated.png").exists(), "the link is not followed");
assert!(!folder.join("deck-1.png").exists(), "nothing of this run stays");
}
#[test]
fn master_view_exports_slides_and_has_no_current() {
use crate::dialog::Dialog;
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;
};
three_slides(&mut cx, &deck);
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));
let window = cx.update(|app| deck.read(app).core.window.expect("a window"));
cx.update_window(window, |_, window, app| {
deck.update(app, |deck, cx| deck.on_export(&ely_ui::commands::Export, window, cx))
})
.expect("the window lives");
settle(&cx, |app| matches!(deck.read(app).dialog, Some(Dialog::Output(_))));
let (current, hidden) = cx.update(|app| match &deck.read(app).dialog {
Some(Dialog::Output(output)) => (output.current, output.hidden.clone()),
_ => unreachable!("settled above"),
});
assert_eq!((current, hidden), (None, vec![false, false, true]));
}
#[test]
fn a_late_check_never_covers_a_newer_dialog() {
use crate::dialog::Dialog;
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;
};
// The export dialog is open before the check comes back.
let open = output(&mut cx, &deck, Kind::Slides, Range::All);
update(&mut cx, &deck, |deck, cx| {
deck.check_show(cx);
deck.dialog = Some(Dialog::Output(open));
});
for _ in 0..200 {
cx.run_until_parked();
}
assert!(cx.update(|app| matches!(deck.read(app).dialog, Some(Dialog::Output(_)))));
}
#[test]
fn a_late_check_stays_away_after_another_dialog_closes() {
use crate::dialog::Dialog;
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 open = output(&mut cx, &deck, Kind::Slides, Range::All);
update(&mut cx, &deck, |deck, cx| {
deck.check_show(cx);
deck.dialog = Some(Dialog::Output(open));
cx.notify();
});
// The other dialog is cancelled before the check returns.
update(&mut cx, &deck, |deck, cx| {
deck.dialog = None;
cx.notify();
});
for _ in 0..200 {
cx.run_until_parked();
}
assert!(cx.update(|app| deck.read(app).dialog.is_none()), "the old check stays away");
}
#[test]
fn pictures_follow_the_slides_as_written_not_as_asked() {
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;
};
three_slides(&mut cx, &deck);
// Asked while slide three hid; shown again before writing.
let asked = output(&mut cx, &deck, Kind::Pictures, Range::All);
assert_eq!(asked.hidden, [false, false, true]);
update(&mut cx, &deck, |deck, cx| deck.design_command("slide.show", &[], cx));
settle(&cx, |app| {
let tab = deck.read(app).active_tab();
tab.is_some_and(|tab| tab.extra.slides.list.iter().all(|slide| slide.visible))
});
let folder = dir.path().join("out");
std::fs::create_dir_all(&folder).expect("a folder");
let into = (folder.clone(), "deck".to_string());
update(&mut cx, &deck, |deck, cx| deck.write_pictures(&asked, into, cx));
let count = |folder: &Path| std::fs::read_dir(folder).expect("reads").count();
settle(&cx, |_| count(&folder) == 3);
for n in 1..=3 {
let picture = image::open(folder.join(format!("deck-{n}.png"))).expect("a readable PNG");
assert_eq!(picture.width(), 1920);
}
}