188 lines
5.7 KiB
Rust
188 lines
5.7 KiB
Rust
use gpui::AppContext as _;
|
||
|
||
use super::*;
|
||
|
||
fn formula(source: &str, display: bool) -> Formula {
|
||
Formula {
|
||
source: source.to_string().into(),
|
||
display,
|
||
rgba: 0x112233ff,
|
||
font: 1600,
|
||
scale: 200,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_formula_typesets_to_outlined_svg_in_its_color() {
|
||
let (svg, shape) = svg(&formula(r"\frac{a}{b} + \sqrt{x}", false)).expect("typesets");
|
||
assert!(svg.starts_with("<svg") && !svg.contains("<text"), "{svg}");
|
||
assert!(svg.contains("17,34,51"), "the color: {svg}");
|
||
assert!(shape.width > px(16.) && shape.height > px(16.), "{shape:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn display_style_sets_limits_larger() {
|
||
let (_, text) = svg(&formula(r"\sum_{k=1}^{n} k", false)).expect("typesets");
|
||
let (_, display) = svg(&formula(r"\sum_{k=1}^{n} k", true)).expect("typesets");
|
||
assert!(display.height > text.height, "{display:?} over {text:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn every_glyph_lies_inside_the_picture() {
|
||
for source in [
|
||
r"\text{私密} x",
|
||
"x",
|
||
r"\int_0^1 f(x)\,dx",
|
||
r"\left(\frac{a}{b}\right)",
|
||
] {
|
||
let (svg, shape) = svg(&formula(source, false)).expect("typesets");
|
||
let tree = usvg::Tree::from_data(svg.as_bytes(), &usvg::Options::default()).expect("svg");
|
||
let ink = tree.root().abs_stroke_bounding_box();
|
||
let (w, h) = (shape.width.as_f32(), shape.height.as_f32());
|
||
let near = |a: f32, b: f32| (a - b).abs() < 0.01;
|
||
assert!(
|
||
near(tree.size().width(), w) && near(tree.size().height(), h),
|
||
"{source}"
|
||
);
|
||
assert!(
|
||
ink.left() >= -0.01 && ink.top() >= -0.01,
|
||
"{source}: ink {ink:?} starts before the picture"
|
||
);
|
||
assert!(
|
||
ink.right() <= w + 0.01 && ink.bottom() <= h + 0.01,
|
||
"{source}: ink {ink:?} passes {w}×{h}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn bad_tex_says_why_and_where() {
|
||
let Err(error) = Latex::parse("x", r"x^2^3") else {
|
||
panic!("a double superscript parses")
|
||
};
|
||
assert_eq!(error.to_string(), "Double superscript at 3");
|
||
assert!(Latex::parse("x", r"x^{2").is_err(), "an open brace");
|
||
}
|
||
|
||
#[test]
|
||
fn a_layout_panic_is_an_error_without_the_panic_text() {
|
||
let laid: Result<(), MathError> = guarded(|| panic!("private formula text"));
|
||
let error = laid.expect_err("the panic is an error");
|
||
assert!(!error.to_string().contains("private"), "{error}");
|
||
}
|
||
|
||
/// Formulas drawn by id; the test changes them.
|
||
struct Shown(Vec<&'static str>);
|
||
|
||
impl gpui::Render for Shown {
|
||
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
|
||
div().children(
|
||
self.0
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(ix, source)| Latex::parse(ix, source).expect("parses")),
|
||
)
|
||
}
|
||
}
|
||
|
||
fn open<'a>(
|
||
sources: Vec<&'static str>,
|
||
cx: &'a mut gpui::TestAppContext,
|
||
) -> (gpui::Entity<Shown>, &'a mut gpui::VisualTestContext) {
|
||
cx.update(crate::theme::Theme::init);
|
||
let (view, cx) = cx.add_window_view(|_, _| Shown(sources));
|
||
settle(cx);
|
||
(view, cx)
|
||
}
|
||
|
||
fn settle(cx: &mut gpui::VisualTestContext) {
|
||
for _ in 0..3 {
|
||
cx.run_until_parked();
|
||
cx.update(|window, _| window.refresh());
|
||
}
|
||
cx.run_until_parked();
|
||
}
|
||
|
||
/// Draws, then lets gpui release what the frame dropped.
|
||
fn flush(cx: &mut gpui::VisualTestContext) {
|
||
cx.run_until_parked();
|
||
cx.update(|_, _| ());
|
||
}
|
||
|
||
/// Whether `source`'s picture sits in the asset cache.
|
||
fn cached(source: &str, cx: &mut gpui::VisualTestContext) -> bool {
|
||
cx.update(|window, cx| {
|
||
let latex = Latex::parse("probe", source).expect("parses");
|
||
let (formula, _) = latex.formula(window, cx);
|
||
cx.has_asset::<Typesetter>(&formula)
|
||
})
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn a_formula_frees_its_picture_when_it_changes_or_leaves(cx: &mut gpui::TestAppContext) {
|
||
let (view, cx) = open(vec!["a^2"], cx);
|
||
assert!(cached("a^2", cx), "drawn once");
|
||
view.update(cx, |view, cx| {
|
||
view.0 = vec!["b^2"];
|
||
cx.notify();
|
||
});
|
||
settle(cx);
|
||
assert!(!cached("a^2", cx) && cached("b^2", cx), "the old one freed");
|
||
view.update(cx, |view, cx| {
|
||
view.0.clear();
|
||
cx.notify();
|
||
});
|
||
settle(cx);
|
||
assert!(!cached("b^2", cx), "gone with its element");
|
||
}
|
||
|
||
/// One formula in a view of its own.
|
||
struct One;
|
||
|
||
impl gpui::Render for One {
|
||
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
|
||
Latex::parse("one", "c^2").expect("parses")
|
||
}
|
||
}
|
||
|
||
/// Views of one formula, each drawn from gpui's view cache.
|
||
struct Pair(Vec<gpui::Entity<One>>);
|
||
|
||
impl gpui::Render for Pair {
|
||
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
|
||
div().children(
|
||
self.0.iter().map(|one| {
|
||
gpui::AnyView::from(one.clone()).cached(gpui::StyleRefinement::default())
|
||
}),
|
||
)
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn a_shared_formula_stays_until_its_last_holder_leaves(cx: &mut gpui::TestAppContext) {
|
||
cx.update(crate::theme::Theme::init);
|
||
let (pair, cx) = cx.add_window_view(|_, cx| Pair(vec![cx.new(|_| One), cx.new(|_| One)]));
|
||
settle(cx);
|
||
assert!(cached("c^2", cx), "drawn");
|
||
pair.update(cx, |pair, cx| {
|
||
pair.0.pop();
|
||
cx.notify();
|
||
});
|
||
flush(cx);
|
||
assert!(cached("c^2", cx), "the cached survivor still holds it");
|
||
pair.update(cx, |pair, cx| {
|
||
pair.0.clear();
|
||
cx.notify();
|
||
});
|
||
flush(cx);
|
||
assert!(!cached("c^2", cx), "the last holder freed it");
|
||
}
|
||
|
||
#[test]
|
||
fn an_exported_formula_is_black_outlines() {
|
||
let svg = formula_svg(r"\sqrt{x}", true).expect("typesets");
|
||
assert!(svg.starts_with("<svg") && !svg.contains("<text"), "{svg}");
|
||
assert!(svg.contains("0,0,0"), "black: {svg}");
|
||
assert!(formula_svg(r"\frac{", false).is_err());
|
||
}
|