mdbook/tests/testsuite/search.rs
2025-07-23 17:47:31 -07:00

144 lines
5.3 KiB
Rust

//! Tests for search support.
use crate::prelude::*;
use mdbook_core::book::{BookItem, Chapter};
use snapbox::file;
use std::path::{Path, PathBuf};
fn read_book_index(root: &Path) -> serde_json::Value {
let index = root.join("book/searchindex.js");
let index = std::fs::read_to_string(index).unwrap();
let index =
index.trim_start_matches("window.search = Object.assign(window.search, JSON.parse('");
let index = index.trim_end_matches("'));");
// We need unescape the string as it's supposed to be an escaped JS string.
serde_json::from_str(&index.replace("\\'", "'").replace("\\\\", "\\")).unwrap()
}
// Some spot checks for the generation of the search index.
#[test]
fn reasonable_search_index() {
let mut test = BookTest::from_dir("search/reasonable_search_index");
test.build();
let index = read_book_index(&test.dir);
let doc_urls = index["doc_urls"].as_array().unwrap();
eprintln!("doc_urls={doc_urls:#?}",);
let get_doc_ref = |url: &str| -> String {
doc_urls
.iter()
.position(|s| s == url)
.unwrap_or_else(|| panic!("failed to find {url}"))
.to_string()
};
let first_chapter = get_doc_ref("first/index.html#first-chapter");
let introduction = get_doc_ref("intro.html#introduction");
let some_section = get_doc_ref("first/index.html#some-section");
let summary = get_doc_ref("first/includes.html#summary");
let no_headers = get_doc_ref("first/no-headers.html");
let duplicate_headers_1 = get_doc_ref("first/duplicate-headers.html#header-text-1");
let heading_attrs = get_doc_ref("first/heading-attributes.html#both");
let sneaky = get_doc_ref("intro.html#sneaky");
let bodyidx = &index["index"]["index"]["body"]["root"];
let textidx = &bodyidx["t"]["e"]["x"]["t"];
assert_eq!(textidx["df"], 5);
assert_eq!(textidx["docs"][&first_chapter]["tf"], 1.0);
assert_eq!(textidx["docs"][&introduction]["tf"], 1.0);
let docs = &index["index"]["documentStore"]["docs"];
assert_eq!(docs[&first_chapter]["body"], "more text.");
assert_eq!(docs[&some_section]["body"], "");
assert_eq!(
docs[&summary]["body"],
"Introduction First Chapter Includes Unicode No Headers Duplicate Headers Heading Attributes"
);
assert_eq!(
docs[&summary]["breadcrumbs"],
"First Chapter » Includes » Summary"
);
// See note about InlineHtml in search.rs. Ideally the `alert()` part
// should not be in the index, but we don't have a way to scrub inline
// html.
assert_eq!(
docs[&sneaky]["body"],
"I put <HTML> in here! Sneaky inline event alert(\"inline\");. But regular inline is indexed."
);
assert_eq!(
docs[&no_headers]["breadcrumbs"],
"First Chapter » No Headers"
);
assert_eq!(
docs[&duplicate_headers_1]["breadcrumbs"],
"First Chapter » Duplicate Headers » Header Text"
);
assert_eq!(
docs[&no_headers]["body"],
"Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex."
);
assert_eq!(
docs[&heading_attrs]["breadcrumbs"],
"First Chapter » Heading Attributes » Heading with id and classes"
);
}
// This test is here to catch any unexpected changes to the search index.
#[test]
fn search_index_hasnt_changed_accidentally() {
BookTest::from_dir("search/reasonable_search_index").check_file(
"book/searchindex.js",
file!["search/reasonable_search_index/expected_index.js"],
);
}
// Ability to disable search chapters.
#[test]
fn can_disable_individual_chapters() {
let mut test = BookTest::from_dir("search/disable_search_chapter");
test.build();
let index = read_book_index(&test.dir);
let doc_urls = index["doc_urls"].as_array().unwrap();
let contains = |path| {
doc_urls
.iter()
.any(|p| p.as_str().unwrap().starts_with(path))
};
assert!(contains("second.html"));
assert!(!contains("second/"));
assert!(!contains("first/disable_me.html"));
assert!(contains("first/keep_me.html"));
}
// Test for a regression where search would fail if source_path is None.
// https://github.com/rust-lang/mdBook/pull/2550
#[test]
fn with_no_source_path() {
let test = BookTest::from_dir("search/reasonable_search_index");
let mut book = test.load_book();
let chapter = Chapter {
name: "Sample chapter".to_string(),
content: "".to_string(),
number: None,
sub_items: Vec::new(),
path: Some(PathBuf::from("sample.html")),
source_path: None,
parent_names: Vec::new(),
};
book.book.sections.push(BookItem::Chapter(chapter));
book.build().unwrap();
}
// Checks that invalid settings in search chapter is rejected.
#[test]
fn chapter_settings_validation_error() {
BookTest::from_dir("search/chapter_settings_validation_error").run("build", |cmd| {
cmd.expect_failure().expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend
[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: [output.html.search.chapter] key `does-not-exist` does not match any chapter paths
"#]]);
});
}