//! Tests for the formula renderer. //! //! They live here rather than inside the module so that `src/` holds no test //! code, as it does in `apsis-rs`. An integration test is a separate crate and //! reaches only what the library makes public, so the few internal items the //! tests need are opened through `apsis_book::testkit`. use apsis_book::docs::mathml::*; use apsis_book::testkit::mathml; #[test] fn a_variable_is_an_identifier() { assert_eq!(mathml("x"), "x"); } #[test] fn a_power_becomes_a_superscript() { assert_eq!( mathml("x^2"), "x2" ); } #[test] fn a_group_holds_several_tokens() { assert_eq!( mathml("x^{12}"), "x12" ); } #[test] fn a_fraction_has_a_numerator_and_a_denominator() { assert_eq!( mathml("\\frac{a}{b}"), "ab" ); } #[test] fn a_greek_letter_is_translated() { assert_eq!(mathml("\\theta"), "θ"); } #[test] fn an_unknown_command_is_kept_as_written() { // Losing part of a formula would be worse than showing it plainly. // Text is given an element even when it is not understood, because text // without one is not drawn at all. assert_eq!(mathml("\\notacommand"), "\\notacommand"); } #[test] fn a_comma_is_kept() { // The command is the spacing one, so dropping it would run a list of // symbols together. It is written as an operator so that it keeps its // space, which is what a comma is for. assert_eq!( mathml("\\alpha, \\beta"), "α,β" ); } #[test] fn a_root_can_have_an_index() { let html = mathml("\\sqrt[3]{x}"); assert!(html.contains(""), "the index was lost: {html}"); } #[test] fn text_is_escaped() { assert!(mathml("a"), "no root here"); } #[test] fn a_subscript_within_a_subscript_is_read() { // `delta_v_1` is delta with a subscript v, itself carrying 1. let html = render_plain("delta_v_1 = 3").expect("a formula"); assert!(html.contains("delta"), "{html}"); assert!(html.contains(""), "the subscript was lost: {html}"); } #[test] fn a_power_is_read() { let html = render_plain("e^2 = 2 f - f^2").expect("a formula"); assert!(html.contains(""), "the power was lost: {html}"); } #[test] fn a_root_is_read() { let html = render_plain("x = sqrt( mu / r )").expect("a formula"); assert!(html.contains(""), "the root was lost: {html}"); } #[test] fn a_quotient_is_taken_as_its_own() { // `2/r_1 - 1/a_t` is two quotients, not one quotient of a difference. let html = render_plain("d = 2/r_1 - 1/a_t").expect("a formula"); assert_eq!(html.matches("").count(), 3, "{html}"); } #[test] fn a_matrix_stays_as_written() { // A matrix is not a formula this renderer lays out. assert!(render_plain("R = [ 1 0\n 0 1 ]").is_none()); } #[test] fn prose_stays_as_written() { assert!(render_plain("e_max such that the elevation is arctan").is_none()); assert!(render_plain("just some text").is_none()); } #[test] fn a_trailing_note_is_not_part_of_the_equation() { // The parenthesis is a phrase, not a bracket in the expression. let html = render_plain("N = a / sqrt(1 - e^2 sin^2 phi) (prime vertical radius)") .expect("a formula"); // The note is set beside the formula rather than inside it, and it is // kept rather than dropped. assert!( html.contains("prime vertical radius"), "the note was lost: {html}" ); assert!( html.contains(""), "{html}"); } #[test] fn a_brace_holds_a_subscript_of_several_symbols() { // The Newton iteration, written the way the documents write it. let html = render_plain("E_{n+1} = E_n - f(E_n)").expect("a formula"); assert!(html.contains(""), "the subscript was lost: {html}"); } #[test] fn a_continuation_line_is_kept_though_it_is_not_an_equation() { // A block of equations often carries a line with no equals sign on it. // Losing that line would lose the equation above it. let html = render_plain("JDN = day + 365 y\n + 153 m").expect("a formula"); assert!(html.contains("JDN"), "{html}"); assert!(html.contains("153 m"), "the continuation was lost: {html}"); } #[test] fn a_block_with_no_readable_equation_is_left_alone() { assert!(render_plain("leap(y) = (y mod 4 == 0) and (y mod 100 != 0").is_none()); } #[test] fn an_absolute_value_may_hold_a_product() { // `| f'(x) | * | x| / | f(x) |` is one expression: a bar inside it // opens the next factor rather than closing the one before. let html = render_plain("kappa = | f'(x) | * | x| / | f(x) |").expect("a formula"); assert!(!html.contains("math-note"), "a note was read: {html}"); } #[test] fn a_power_may_be_a_word() { // `R^alpha` writes the exponent as a name rather than a digit. assert!(render_plain("gamma = k * R^alpha / (cos theta)").is_some()); } #[test] fn a_trailing_bracket_of_the_equation_is_not_a_note() { // `(cos theta)` is part of the equation even though it holds only // words; the whole line reads out, so there is no note. let html = render_plain("gamma = k * R^alpha / (cos theta)").expect("a formula"); assert!(!html.contains("math-note"), "a note was read: {html}"); } #[test] fn a_bracketed_group_is_not_mistaken_for_a_note() { // `(m * g)` holds an operator, so it is part of the equation; a note // is a phrase and has none. let html = render_plain("H = k * T / (m * g)").expect("a formula"); assert!(html.contains("\u{2212}"), "the minus was lost: {html}" ); assert!(html.contains("="), "the equals was lost: {html}"); } #[test] fn an_equals_between_two_equations_is_kept() { let html = render_plain("r = p / (1 + e cos nu), p = a (1 - e^2)").expect("a formula"); assert!(html.contains("="), "{html}"); assert!(html.contains("\u{2212}"), "{html}"); } #[test] fn a_number_of_several_digits_is_kept() { // Only single digits were given an element, and text without one is // dropped by the browser, so `14`, `4800` and `32045` vanished from // every formula that held them. let html = render_plain("JDN = day + floor((153 m + 2) / 5) + 365 y").expect("a formula"); for number in ["153", "2", "5", "365"] { assert!( html.contains(&format!("{number}")), "{number} has no element: {html}" ); } // Each number is inside an element; text without one is not drawn. assert!(html.matches("").count() >= 4, "{html}"); } #[test] fn a_decimal_and_an_exponent_are_single_numbers() { let html = render_plain("x = 2400000.5 + 1.5e-3").expect("a formula"); assert!(html.contains("2400000.5"), "{html}"); assert!(html.contains("1.5e-3"), "{html}"); } #[test] fn a_row_holds_elements_rather_than_text() { // Bare text inside a row is dropped by the browser, so a number or a name // has to be given an element. The end to end check compares the // characters the reader sees against the source; this one states the // shape that makes that true, so a failure points at the cause. let html = render_plain("a = floor((14 - month) / 12)").expect("a formula"); let body = html.split("'); let tag = parts.next().unwrap_or_default(); let after = parts.next().unwrap_or_default(); let text: String = after.chars().take_while(|c| *c != '<').collect(); if text.is_empty() { continue; } let name = tag .trim_start_matches('/') .split(' ') .next() .unwrap_or_default(); assert!( matches!(name, "mi" | "mo" | "mn" | "mtext"), "text {text:?} sits in <{name}>, which does not draw it: {html}" ); } } #[test] fn a_bracketed_group_takes_the_power_that_follows_it() { // The bracket branches returned before the scripts were attached, so the // caret was set as an ordinary symbol and the exponent sat beside the // group: `(a + b)^2` showed its own caret. for latex in [ r"(a + b)^2", r"\left( \frac{x_{ke}}{n_0} \right)^{2/3}", r"\left[ a \right]^2", r"\{a\}_2", ] { let html = render_plain_of(latex); assert!( html.contains("") || html.contains(""), "{latex} kept its script marker: {html}" ); assert!( !html.contains("^") && !html.contains("_"), "{latex} left a script marker in the output: {html}" ); } } /// The MathML of a formula written in LaTeX. fn render_plain_of(latex: &str) -> String { apsis_book::docs::mathml::render(latex, false) .split("display=\"inline\">") .nth(1) .unwrap_or("") .split("") .next() .unwrap_or("") .to_string() }