201 lines
6.8 KiB
Rust
201 lines
6.8 KiB
Rust
//! Tests for the Markdown 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::markdown::*;
|
|
|
|
#[test]
|
|
fn an_inline_formula_is_rendered() {
|
|
let html = render("The rate is $r$ per second.");
|
|
assert!(html.contains("<math"), "the formula was not rendered");
|
|
assert!(!html.contains("math-display"), "it belongs in the sentence");
|
|
assert!(html.contains("per second."), "the sentence was lost");
|
|
}
|
|
|
|
#[test]
|
|
fn a_display_formula_is_rendered_on_its_own() {
|
|
let html = render("$$a^2 + b^2 = c^2$$");
|
|
assert!(html.contains("math-display"), "the formula is not a block");
|
|
assert!(html.contains("display=\"block\""), "it is not a display");
|
|
}
|
|
|
|
#[test]
|
|
fn a_dollar_without_a_pair_is_left_alone() {
|
|
// A price must survive as a price.
|
|
let html = render("It costs $5 today.");
|
|
assert!(html.contains("$5"), "the lone dollar was consumed");
|
|
assert!(!html.contains("<math"), "no formula should be produced");
|
|
}
|
|
|
|
#[test]
|
|
fn two_prices_are_not_a_formula() {
|
|
// The closing dollar has a space before it, which is what separates
|
|
// this from a formula.
|
|
let html = render("It costs $5 and then $6.");
|
|
assert!(
|
|
!html.contains("<math"),
|
|
"two prices should not be a formula"
|
|
);
|
|
assert!(html.contains("$5 and then $6."), "the prices were consumed");
|
|
}
|
|
|
|
#[test]
|
|
fn a_dollar_inside_code_is_left_alone() {
|
|
let html = render("`$HOME` is the home directory.");
|
|
assert!(!html.contains("<math"), "code should not become a formula");
|
|
assert!(
|
|
html.contains("<code>$HOME</code>"),
|
|
"the code span was lost"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_formula_does_not_cross_a_line() {
|
|
let html = render("A $x$ and\nB $y$.");
|
|
assert_eq!(html.matches("<math").count(), 2, "each should be its own");
|
|
}
|
|
|
|
#[test]
|
|
fn a_backslash_in_a_formula_is_kept() {
|
|
let html = render("$\\frac{1}{2}$");
|
|
assert!(html.contains("<mfrac>"), "the fraction was not built");
|
|
}
|
|
|
|
#[test]
|
|
fn renders_headings_and_paragraphs() {
|
|
let html = render("# Title\n\nSome *text* and **more**.\n");
|
|
assert!(html.contains("<h1 id=\"title\">Title"));
|
|
assert!(html.contains("<p>Some <em>text</em> and <strong>more</strong>.</p>"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_tables_with_alignment() {
|
|
let markdown = "| a | b |\n|---|---|\n| 1 | 2 |\n";
|
|
let html = render(markdown);
|
|
assert!(html.contains("<th>a</th>"));
|
|
assert!(html.contains("<td>1</td>"));
|
|
}
|
|
|
|
#[test]
|
|
fn escapes_code_and_text() {
|
|
let html = render("Use `a < b` and <script>.\n");
|
|
assert!(html.contains("<code>a < b</code>"));
|
|
assert!(html.contains("<script>"));
|
|
assert!(!html.contains("<script>"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_rust_path_link_points_at_the_interface_page() {
|
|
let html = render("Returns [`Epoch`](crate::Epoch) and [more](https://example.com).\n");
|
|
assert!(html.contains("data-api=\"Epoch\""), "{html}");
|
|
assert!(html.contains("href=\"#/api/Epoch\""), "{html}");
|
|
// A web address is left as it is.
|
|
assert!(html.contains("href=\"https://example.com\""), "{html}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_path_to_a_variant_resolves_to_the_declaring_item() {
|
|
let known: std::collections::HashSet<String> = ["PropagationErrorKind"]
|
|
.iter()
|
|
.map(|n| n.to_string())
|
|
.collect();
|
|
let html = render_with(
|
|
"Returns [`Decayed`](crate::PropagationErrorKind::Decayed).\n",
|
|
Some(&known),
|
|
);
|
|
assert!(html.contains("data-api=\"PropagationErrorKind\""), "{html}");
|
|
// A path whose segments are all unknown is left as written.
|
|
let html = render_with("[`Nope`](crate::Unknown::Missing).\n", Some(&known));
|
|
assert!(!html.contains("data-api"), "{html}");
|
|
}
|
|
|
|
/// The names of two items, for the shortcut link test.
|
|
fn items() -> std::collections::HashSet<String> {
|
|
["mjd", "Epoch"]
|
|
.iter()
|
|
.map(|name| name.to_string())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn a_shortcut_link_points_at_the_interface_page() {
|
|
let known = items();
|
|
let html = render_with(
|
|
"See [`Epoch::mjd`] and [`apsis_time::Epoch`].\n",
|
|
Some(&known),
|
|
);
|
|
assert!(html.contains("data-api=\"mjd\""), "{html}");
|
|
assert!(html.contains("href=\"#/api/mjd\""), "{html}");
|
|
assert!(html.contains("data-api=\"Epoch\""), "{html}");
|
|
// A label that is not an item name stays as a code span.
|
|
let html = render_with("The range is [`0, 1`].\n", Some(&known));
|
|
assert!(!html.contains("data-api"), "{html}");
|
|
assert!(html.contains("<code>0, 1</code>"), "{html}");
|
|
// A name that is not in the catalog also stays as a code span.
|
|
let html = render_with("See [`NotAnItem`].\n", Some(&known));
|
|
assert!(!html.contains("data-api"), "{html}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_chinese_heading_keeps_its_own_anchor() {
|
|
// Only ASCII letters were kept, so every Chinese heading reduced to
|
|
// the fallback and the whole table of contents pointed at the first
|
|
// section.
|
|
let html = render("# 时间尺度\n\n正文。\n\n# 单位约定\n\n更多。\n");
|
|
assert!(html.contains("id=\"时间尺度\""), "{html}");
|
|
assert!(html.contains("id=\"单位约定\""), "{html}");
|
|
assert!(!html.contains("id=\"section\""), "{html}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_repeated_heading_gets_a_distinct_anchor() {
|
|
let html = render("# 说明\n\n甲。\n\n# 说明\n\n乙。\n");
|
|
assert!(html.contains("id=\"说明\""), "{html}");
|
|
assert!(html.contains("id=\"说明-2\""), "{html}");
|
|
}
|
|
|
|
#[test]
|
|
fn renders_concept_links() {
|
|
let html = render("See [[sgp4]] and [[tle|the element set]].\n");
|
|
assert!(html.contains("data-concept=\"sgp4\""));
|
|
assert!(html.contains(">the element set</a>"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_code_blocks_with_language() {
|
|
let html = render("```rust\nlet x = 1;\n```\n");
|
|
assert!(html.contains("class=\"language-rust\""));
|
|
assert!(html.contains("let x = 1;"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_lists_and_quotes() {
|
|
let html = render("- one\n- two\n\n> quoted\n");
|
|
assert!(html.contains("<ul>"));
|
|
assert!(html.contains("<li>one</li>"));
|
|
assert!(html.contains("<blockquote>"));
|
|
}
|
|
|
|
#[test]
|
|
fn plain_text_drops_the_markup() {
|
|
let text = to_plain_text("# Title\n\nSee [[sgp4|SGP4]] and `code`.\n");
|
|
assert!(text.contains("Title"));
|
|
assert!(text.contains("SGP4"));
|
|
assert!(!text.contains("[["));
|
|
assert!(!text.contains('`'));
|
|
}
|
|
|
|
#[test]
|
|
fn plain_text_resolves_a_shortcut_reference() {
|
|
// A rustdoc shortcut keeps the item name and loses the brackets,
|
|
// which otherwise appear verbatim in a one line summary.
|
|
let text = to_plain_text("See [`Vec3`] and [`Epoch::mjd`](crate::Epoch::mjd).\n");
|
|
assert!(text.contains("Vec3"));
|
|
assert!(text.contains("mjd"));
|
|
assert!(!text.contains('['), "{text}");
|
|
assert!(!text.contains('`'), "{text}");
|
|
}
|