1 #[macro_use]
2 extern crate pretty_assertions;
3 
4 mod dummy_book;
5 
6 use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
7 
8 use anyhow::Context;
9 use mdbook::config::Config;
10 use mdbook::errors::*;
11 use mdbook::utils::fs::write_file;
12 use mdbook::MDBook;
13 use select::document::Document;
14 use select::predicate::{Class, Name, Predicate};
15 use std::collections::HashMap;
16 use std::ffi::OsStr;
17 use std::fs;
18 use std::io::Write;
19 use std::path::{Component, Path, PathBuf};
20 use tempfile::Builder as TempFileBuilder;
21 use walkdir::{DirEntry, WalkDir};
22 
23 const BOOK_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
24 const TOC_TOP_LEVEL: &[&str] = &[
25     "1. First Chapter",
26     "2. Second Chapter",
27     "Conclusion",
28     "Dummy Book",
29     "Introduction",
30 ];
31 const TOC_SECOND_LEVEL: &[&str] = &[
32     "1.1. Nested Chapter",
33     "1.2. Includes",
34     "1.3. Recursive",
35     "1.4. Markdown",
36     "1.5. Unicode",
37     "1.6. No Headers",
38     "2.1. Nested Chapter",
39 ];
40 
41 /// Make sure you can load the dummy book and build it without panicking.
42 #[test]
build_the_dummy_book()43 fn build_the_dummy_book() {
44     let temp = DummyBook::new().build().unwrap();
45     let md = MDBook::load(temp.path()).unwrap();
46 
47     md.build().unwrap();
48 }
49 
50 #[test]
by_default_mdbook_generates_rendered_content_in_the_book_directory()51 fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
52     let temp = DummyBook::new().build().unwrap();
53     let md = MDBook::load(temp.path()).unwrap();
54 
55     assert!(!temp.path().join("book").exists());
56     md.build().unwrap();
57 
58     assert!(temp.path().join("book").exists());
59     let index_file = md.build_dir_for("html").join("index.html");
60     assert!(index_file.exists());
61 }
62 
63 #[test]
make_sure_bottom_level_files_contain_links_to_chapters()64 fn make_sure_bottom_level_files_contain_links_to_chapters() {
65     let temp = DummyBook::new().build().unwrap();
66     let md = MDBook::load(temp.path()).unwrap();
67     md.build().unwrap();
68 
69     let dest = temp.path().join("book");
70     let links = vec![
71         r#"href="intro.html""#,
72         r#"href="first/index.html""#,
73         r#"href="first/nested.html""#,
74         r#"href="second.html""#,
75         r#"href="conclusion.html""#,
76     ];
77 
78     let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"];
79 
80     for filename in files_in_bottom_dir {
81         assert_contains_strings(dest.join(filename), &links);
82     }
83 }
84 
85 #[test]
check_correct_cross_links_in_nested_dir()86 fn check_correct_cross_links_in_nested_dir() {
87     let temp = DummyBook::new().build().unwrap();
88     let md = MDBook::load(temp.path()).unwrap();
89     md.build().unwrap();
90 
91     let first = temp.path().join("book").join("first");
92     let links = vec![
93         r#"href="../intro.html""#,
94         r#"href="../first/index.html""#,
95         r#"href="../first/nested.html""#,
96         r#"href="../second.html""#,
97         r#"href="../conclusion.html""#,
98     ];
99 
100     let files_in_nested_dir = vec!["index.html", "nested.html"];
101 
102     for filename in files_in_nested_dir {
103         assert_contains_strings(first.join(filename), &links);
104     }
105 
106     assert_contains_strings(
107         first.join("index.html"),
108         &[r##"<h2 id="some-section"><a class="header" href="#some-section">"##],
109     );
110 
111     assert_contains_strings(
112         first.join("nested.html"),
113         &[r##"<h2 id="some-section"><a class="header" href="#some-section">"##],
114     );
115 }
116 
117 #[test]
check_correct_relative_links_in_print_page()118 fn check_correct_relative_links_in_print_page() {
119     let temp = DummyBook::new().build().unwrap();
120     let md = MDBook::load(temp.path()).unwrap();
121     md.build().unwrap();
122 
123     let first = temp.path().join("book");
124 
125     assert_contains_strings(
126         first.join("print.html"),
127         &[
128             r##"<a href="second/../first/nested.html">the first section</a>,"##,
129             r##"<a href="second/../../std/foo/bar.html">outside</a>"##,
130             r##"<img src="second/../images/picture.png" alt="Some image" />"##,
131             r##"<a href="second/nested.html#some-section">fragment link</a>"##,
132             r##"<a href="second/../first/markdown.html">HTML Link</a>"##,
133             r##"<img src="second/../images/picture.png" alt="raw html">"##,
134         ],
135     );
136 }
137 
138 #[test]
rendered_code_has_playground_stuff()139 fn rendered_code_has_playground_stuff() {
140     let temp = DummyBook::new().build().unwrap();
141     let md = MDBook::load(temp.path()).unwrap();
142     md.build().unwrap();
143 
144     let nested = temp.path().join("book/first/nested.html");
145     let playground_class = vec![r#"class="playground""#];
146 
147     assert_contains_strings(nested, &playground_class);
148 
149     let book_js = temp.path().join("book/book.js");
150     assert_contains_strings(book_js, &[".playground"]);
151 }
152 
153 #[test]
anchors_include_text_between_but_not_anchor_comments()154 fn anchors_include_text_between_but_not_anchor_comments() {
155     let temp = DummyBook::new().build().unwrap();
156     let md = MDBook::load(temp.path()).unwrap();
157     md.build().unwrap();
158 
159     let nested = temp.path().join("book/first/nested.html");
160     let text_between_anchors = vec!["unique-string-for-anchor-test"];
161     let anchor_text = vec!["ANCHOR"];
162 
163     assert_contains_strings(nested.clone(), &text_between_anchors);
164     assert_doesnt_contain_strings(nested, &anchor_text);
165 }
166 
167 #[test]
rustdoc_include_hides_the_unspecified_part_of_the_file()168 fn rustdoc_include_hides_the_unspecified_part_of_the_file() {
169     let temp = DummyBook::new().build().unwrap();
170     let md = MDBook::load(temp.path()).unwrap();
171     md.build().unwrap();
172 
173     let nested = temp.path().join("book/first/nested.html");
174     let text = vec![
175         "<span class=\"boring\">fn some_function() {",
176         "<span class=\"boring\">fn some_other_function() {",
177     ];
178 
179     assert_contains_strings(nested, &text);
180 }
181 
182 #[test]
chapter_content_appears_in_rendered_document()183 fn chapter_content_appears_in_rendered_document() {
184     let content = vec![
185         ("index.html", "This file is just here to cause the"),
186         ("intro.html", "Here's some interesting text"),
187         ("second.html", "Second Chapter"),
188         ("first/nested.html", "testable code"),
189         ("first/index.html", "more text"),
190         ("conclusion.html", "Conclusion"),
191     ];
192 
193     let temp = DummyBook::new().build().unwrap();
194     let md = MDBook::load(temp.path()).unwrap();
195     md.build().unwrap();
196 
197     let destination = temp.path().join("book");
198 
199     for (filename, text) in content {
200         let path = destination.join(filename);
201         assert_contains_strings(path, &[text]);
202     }
203 }
204 
205 /// Apply a series of predicates to some root predicate, where each
206 /// successive predicate is the descendant of the last one. Similar to how you
207 /// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list.
208 macro_rules! descendants {
209     ($root:expr, $($child:expr),*) => {
210         $root
211         $(
212             .descendant($child)
213         )*
214     };
215 }
216 
217 /// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered
218 /// and placed in the `book` directory with their extensions set to `*.html`.
219 #[test]
chapter_files_were_rendered_to_html()220 fn chapter_files_were_rendered_to_html() {
221     let temp = DummyBook::new().build().unwrap();
222     let src = Path::new(BOOK_ROOT).join("src");
223 
224     let chapter_files = WalkDir::new(&src)
225         .into_iter()
226         .filter_entry(|entry| entry_ends_with(entry, ".md"))
227         .filter_map(std::result::Result::ok)
228         .map(|entry| entry.path().to_path_buf())
229         .filter(|path| path.file_name().and_then(OsStr::to_str) != Some("SUMMARY.md"));
230 
231     for chapter in chapter_files {
232         let rendered_location = temp
233             .path()
234             .join(chapter.strip_prefix(&src).unwrap())
235             .with_extension("html");
236         assert!(
237             rendered_location.exists(),
238             "{} doesn't exits",
239             rendered_location.display()
240         );
241     }
242 }
243 
entry_ends_with(entry: &DirEntry, ending: &str) -> bool244 fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
245     entry.file_name().to_string_lossy().ends_with(ending)
246 }
247 
248 /// Read the main page (`book/index.html`) and expose it as a DOM which we
249 /// can search with the `select` crate
root_index_html() -> Result<Document>250 fn root_index_html() -> Result<Document> {
251     let temp = DummyBook::new()
252         .build()
253         .with_context(|| "Couldn't create the dummy book")?;
254     MDBook::load(temp.path())?
255         .build()
256         .with_context(|| "Book building failed")?;
257 
258     let index_page = temp.path().join("book").join("index.html");
259     let html = fs::read_to_string(&index_page).with_context(|| "Unable to read index.html")?;
260 
261     Ok(Document::from(html.as_str()))
262 }
263 
264 #[test]
check_second_toc_level()265 fn check_second_toc_level() {
266     let doc = root_index_html().unwrap();
267     let mut should_be = Vec::from(TOC_SECOND_LEVEL);
268     should_be.sort_unstable();
269 
270     let pred = descendants!(
271         Class("chapter"),
272         Name("li"),
273         Name("li"),
274         Name("a").and(Class("toggle").not())
275     );
276 
277     let mut children_of_children: Vec<_> = doc
278         .find(pred)
279         .map(|elem| elem.text().trim().to_string())
280         .collect();
281     children_of_children.sort();
282 
283     assert_eq!(children_of_children, should_be);
284 }
285 
286 #[test]
check_first_toc_level()287 fn check_first_toc_level() {
288     let doc = root_index_html().unwrap();
289     let mut should_be = Vec::from(TOC_TOP_LEVEL);
290 
291     should_be.extend(TOC_SECOND_LEVEL);
292     should_be.sort_unstable();
293 
294     let pred = descendants!(
295         Class("chapter"),
296         Name("li"),
297         Name("a").and(Class("toggle").not())
298     );
299 
300     let mut children: Vec<_> = doc
301         .find(pred)
302         .map(|elem| elem.text().trim().to_string())
303         .collect();
304     children.sort();
305 
306     assert_eq!(children, should_be);
307 }
308 
309 #[test]
check_spacers()310 fn check_spacers() {
311     let doc = root_index_html().unwrap();
312     let should_be = 2;
313 
314     let num_spacers = doc
315         .find(Class("chapter").descendant(Name("li").and(Class("spacer"))))
316         .count();
317     assert_eq!(num_spacers, should_be);
318 }
319 
320 /// Ensure building fails if `create-missing` is false and one of the files does
321 /// not exist.
322 #[test]
failure_on_missing_file()323 fn failure_on_missing_file() {
324     let temp = DummyBook::new().build().unwrap();
325     fs::remove_file(temp.path().join("src").join("intro.md")).unwrap();
326 
327     let mut cfg = Config::default();
328     cfg.build.create_missing = false;
329 
330     let got = MDBook::load_with_config(temp.path(), cfg);
331     assert!(got.is_err());
332 }
333 
334 /// Ensure a missing file is created if `create-missing` is true.
335 #[test]
create_missing_file_with_config()336 fn create_missing_file_with_config() {
337     let temp = DummyBook::new().build().unwrap();
338     fs::remove_file(temp.path().join("src").join("intro.md")).unwrap();
339 
340     let mut cfg = Config::default();
341     cfg.build.create_missing = true;
342 
343     assert!(!temp.path().join("src").join("intro.md").exists());
344     let _md = MDBook::load_with_config(temp.path(), cfg).unwrap();
345     assert!(temp.path().join("src").join("intro.md").exists());
346 }
347 
348 /// This makes sure you can include a Rust file with `{{#playground example.rs}}`.
349 /// Specification is in `guide/src/format/rust.md`
350 #[test]
able_to_include_playground_files_in_chapters()351 fn able_to_include_playground_files_in_chapters() {
352     let temp = DummyBook::new().build().unwrap();
353     let md = MDBook::load(temp.path()).unwrap();
354     md.build().unwrap();
355 
356     let second = temp.path().join("book/second.html");
357 
358     let playground_strings = &[
359         r#"class="playground""#,
360         r#"println!(&quot;Hello World!&quot;);"#,
361     ];
362 
363     assert_contains_strings(&second, playground_strings);
364     assert_doesnt_contain_strings(&second, &["{{#playground example.rs}}"]);
365 }
366 
367 /// This makes sure you can include a Rust file with `{{#include ../SUMMARY.md}}`.
368 #[test]
able_to_include_files_in_chapters()369 fn able_to_include_files_in_chapters() {
370     let temp = DummyBook::new().build().unwrap();
371     let md = MDBook::load(temp.path()).unwrap();
372     md.build().unwrap();
373 
374     let includes = temp.path().join("book/first/includes.html");
375 
376     let summary_strings = &[
377         r##"<h1 id="summary"><a class="header" href="#summary">Summary</a></h1>"##,
378         ">First Chapter</a>",
379     ];
380     assert_contains_strings(&includes, summary_strings);
381 
382     assert_doesnt_contain_strings(&includes, &["{{#include ../SUMMARY.md::}}"]);
383 }
384 
385 /// Ensure cyclic includes are capped so that no exceptions occur
386 #[test]
recursive_includes_are_capped()387 fn recursive_includes_are_capped() {
388     let temp = DummyBook::new().build().unwrap();
389     let md = MDBook::load(temp.path()).unwrap();
390     md.build().unwrap();
391 
392     let recursive = temp.path().join("book/first/recursive.html");
393     let content = &["Around the world, around the world
394 Around the world, around the world
395 Around the world, around the world"];
396     assert_contains_strings(&recursive, content);
397 }
398 
399 #[test]
example_book_can_build()400 fn example_book_can_build() {
401     let example_book_dir = dummy_book::new_copy_of_example_book().unwrap();
402 
403     let md = MDBook::load(example_book_dir.path()).unwrap();
404 
405     md.build().unwrap();
406 }
407 
408 #[test]
book_with_a_reserved_filename_does_not_build()409 fn book_with_a_reserved_filename_does_not_build() {
410     let tmp_dir = TempFileBuilder::new().prefix("mdBook").tempdir().unwrap();
411     let src_path = tmp_dir.path().join("src");
412     fs::create_dir(&src_path).unwrap();
413 
414     let summary_path = src_path.join("SUMMARY.md");
415     let print_path = src_path.join("print.md");
416 
417     fs::File::create(print_path).unwrap();
418     let mut summary_file = fs::File::create(summary_path).unwrap();
419     writeln!(summary_file, "[print](print.md)").unwrap();
420 
421     let md = MDBook::load(tmp_dir.path()).unwrap();
422     let got = md.build();
423     assert!(got.is_err());
424 }
425 
426 #[test]
by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index()427 fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() {
428     let temp = DummyBook::new().build().unwrap();
429     let mut cfg = Config::default();
430     cfg.set("book.src", "src2")
431         .expect("Couldn't set config.book.src to \"src2\".");
432     let md = MDBook::load_with_config(temp.path(), cfg).unwrap();
433     md.build().unwrap();
434 
435     let first_index = temp.path().join("book").join("first").join("index.html");
436     let expected_strings = vec![
437         r#"href="../first/index.html""#,
438         r#"href="../second/index.html""#,
439         "First README",
440     ];
441     assert_contains_strings(&first_index, &expected_strings);
442     assert_doesnt_contain_strings(&first_index, &["README.html"]);
443 
444     let second_index = temp.path().join("book").join("second").join("index.html");
445     let unexpected_strings = vec!["Second README"];
446     assert_doesnt_contain_strings(&second_index, &unexpected_strings);
447 }
448 
449 #[test]
theme_dir_overrides_work_correctly()450 fn theme_dir_overrides_work_correctly() {
451     let book_dir = dummy_book::new_copy_of_example_book().unwrap();
452     let book_dir = book_dir.path();
453     let theme_dir = book_dir.join("theme");
454 
455     let mut index = mdbook::theme::INDEX.to_vec();
456     index.extend_from_slice(b"\n<!-- This is a modified index.hbs! -->");
457 
458     write_file(&theme_dir, "index.hbs", &index).unwrap();
459 
460     let md = MDBook::load(book_dir).unwrap();
461     md.build().unwrap();
462 
463     let built_index = book_dir.join("book").join("index.html");
464     dummy_book::assert_contains_strings(built_index, &["This is a modified index.hbs!"]);
465 }
466 
467 #[test]
no_index_for_print_html()468 fn no_index_for_print_html() {
469     let temp = DummyBook::new().build().unwrap();
470     let md = MDBook::load(temp.path()).unwrap();
471     md.build().unwrap();
472 
473     let print_html = temp.path().join("book/print.html");
474     assert_contains_strings(print_html, &[r##"noindex"##]);
475 
476     let index_html = temp.path().join("book/index.html");
477     assert_doesnt_contain_strings(index_html, &[r##"noindex"##]);
478 }
479 
480 #[test]
markdown_options()481 fn markdown_options() {
482     let temp = DummyBook::new().build().unwrap();
483     let md = MDBook::load(temp.path()).unwrap();
484     md.build().unwrap();
485 
486     let path = temp.path().join("book/first/markdown.html");
487     assert_contains_strings(
488         &path,
489         &[
490             "<th>foo</th>",
491             "<th>bar</th>",
492             "<td>baz</td>",
493             "<td>bim</td>",
494         ],
495     );
496     assert_contains_strings(
497         &path,
498         &[
499             r##"<sup class="footnote-reference"><a href="#1">1</a></sup>"##,
500             r##"<sup class="footnote-reference"><a href="#word">2</a></sup>"##,
501             r##"<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>"##,
502             r##"<div class="footnote-definition" id="word"><sup class="footnote-definition-label">2</sup>"##,
503         ],
504     );
505     assert_contains_strings(&path, &["<del>strikethrough example</del>"]);
506     assert_contains_strings(
507         &path,
508         &[
509             "<li><input disabled=\"\" type=\"checkbox\" checked=\"\"/>\nApples",
510             "<li><input disabled=\"\" type=\"checkbox\" checked=\"\"/>\nBroccoli",
511             "<li><input disabled=\"\" type=\"checkbox\"/>\nCarrots",
512         ],
513     );
514 }
515 
516 #[test]
redirects_are_emitted_correctly()517 fn redirects_are_emitted_correctly() {
518     let temp = DummyBook::new().build().unwrap();
519     let mut md = MDBook::load(temp.path()).unwrap();
520 
521     // override the "outputs.html.redirect" table
522     let redirects: HashMap<PathBuf, String> = vec![
523         (PathBuf::from("/overview.html"), String::from("index.html")),
524         (
525             PathBuf::from("/nexted/page.md"),
526             String::from("https://rust-lang.org/"),
527         ),
528     ]
529     .into_iter()
530     .collect();
531     md.config.set("output.html.redirect", &redirects).unwrap();
532 
533     md.build().unwrap();
534 
535     for (original, redirect) in &redirects {
536         let mut redirect_file = md.build_dir_for("html");
537         // append everything except the bits that make it absolute
538         // (e.g. "/" or "C:\")
539         redirect_file.extend(remove_absolute_components(original));
540         let contents = fs::read_to_string(&redirect_file).unwrap();
541         assert!(contents.contains(redirect));
542     }
543 }
544 
545 #[test]
edit_url_has_default_src_dir_edit_url()546 fn edit_url_has_default_src_dir_edit_url() {
547     let temp = DummyBook::new().build().unwrap();
548     let book_toml = r#"
549         [book]
550         title = "implicit"
551 
552         [output.html]
553         edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
554         "#;
555 
556     write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
557 
558     let md = MDBook::load(temp.path()).unwrap();
559     md.build().unwrap();
560 
561     let index_html = temp.path().join("book").join("index.html");
562     assert_contains_strings(
563         index_html,
564         &[
565             r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src/README.md" title="Suggest an edit""#,
566         ],
567     );
568 }
569 
570 #[test]
edit_url_has_configured_src_dir_edit_url()571 fn edit_url_has_configured_src_dir_edit_url() {
572     let temp = DummyBook::new().build().unwrap();
573     let book_toml = r#"
574         [book]
575         title = "implicit"
576         src = "src2"
577 
578         [output.html]
579         edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
580         "#;
581 
582     write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
583 
584     let md = MDBook::load(temp.path()).unwrap();
585     md.build().unwrap();
586 
587     let index_html = temp.path().join("book").join("index.html");
588     assert_contains_strings(
589         index_html,
590         &[
591             r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src2/README.md" title="Suggest an edit""#,
592         ],
593     );
594 }
595 
remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> + '_596 fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> + '_ {
597     path.components().skip_while(|c| match c {
598         Component::Prefix(_) | Component::RootDir => true,
599         _ => false,
600     })
601 }
602 
603 #[cfg(feature = "search")]
604 mod search {
605     use crate::dummy_book::DummyBook;
606     use mdbook::MDBook;
607     use std::fs::{self, File};
608     use std::path::Path;
609 
read_book_index(root: &Path) -> serde_json::Value610     fn read_book_index(root: &Path) -> serde_json::Value {
611         let index = root.join("book/searchindex.js");
612         let index = fs::read_to_string(index).unwrap();
613         let index = index.trim_start_matches("Object.assign(window.search, ");
614         let index = index.trim_end_matches(");");
615         serde_json::from_str(index).unwrap()
616     }
617 
618     #[test]
619     #[allow(clippy::float_cmp)]
book_creates_reasonable_search_index()620     fn book_creates_reasonable_search_index() {
621         let temp = DummyBook::new().build().unwrap();
622         let md = MDBook::load(temp.path()).unwrap();
623         md.build().unwrap();
624 
625         let index = read_book_index(temp.path());
626 
627         let doc_urls = index["doc_urls"].as_array().unwrap();
628         let get_doc_ref =
629             |url: &str| -> String { doc_urls.iter().position(|s| s == url).unwrap().to_string() };
630 
631         let first_chapter = get_doc_ref("first/index.html#first-chapter");
632         let introduction = get_doc_ref("intro.html#introduction");
633         let some_section = get_doc_ref("first/index.html#some-section");
634         let summary = get_doc_ref("first/includes.html#summary");
635         let no_headers = get_doc_ref("first/no-headers.html");
636         let conclusion = get_doc_ref("conclusion.html#conclusion");
637 
638         let bodyidx = &index["index"]["index"]["body"]["root"];
639         let textidx = &bodyidx["t"]["e"]["x"]["t"];
640         assert_eq!(textidx["df"], 2);
641         assert_eq!(textidx["docs"][&first_chapter]["tf"], 1.0);
642         assert_eq!(textidx["docs"][&introduction]["tf"], 1.0);
643 
644         let docs = &index["index"]["documentStore"]["docs"];
645         assert_eq!(docs[&first_chapter]["body"], "more text.");
646         assert_eq!(docs[&some_section]["body"], "");
647         assert_eq!(
648             docs[&summary]["body"],
649             "Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Second Chapter Nested Chapter Conclusion"
650         );
651         assert_eq!(
652             docs[&summary]["breadcrumbs"],
653             "First Chapter » Includes » Summary"
654         );
655         assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here!");
656         assert_eq!(
657             docs[&no_headers]["breadcrumbs"],
658             "First Chapter » No Headers"
659         );
660         assert_eq!(
661             docs[&no_headers]["body"],
662             "Capybara capybara capybara. Capybara capybara capybara."
663         );
664     }
665 
666     // Setting this to `true` may cause issues with `cargo watch`,
667     // since it may not finish writing the fixture before the tests
668     // are run again.
669     const GENERATE_FIXTURE: bool = false;
670 
get_fixture() -> serde_json::Value671     fn get_fixture() -> serde_json::Value {
672         if GENERATE_FIXTURE {
673             let temp = DummyBook::new().build().unwrap();
674             let md = MDBook::load(temp.path()).unwrap();
675             md.build().unwrap();
676 
677             let src = read_book_index(temp.path());
678 
679             let dest = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/searchindex_fixture.json");
680             let dest = File::create(&dest).unwrap();
681             serde_json::to_writer_pretty(dest, &src).unwrap();
682 
683             src
684         } else {
685             let json = include_str!("searchindex_fixture.json");
686             serde_json::from_str(json).expect("Unable to deserialize the fixture")
687         }
688     }
689 
690     // So you've broken the test. If you changed dummy_book, it's probably
691     // safe to regenerate the fixture. If you haven't then make sure that the
692     // search index still works. Run `cargo run -- serve tests/dummy_book`
693     // and try some searches. Are you getting results? Do the teasers look OK?
694     // Are there new errors in the JS console?
695     //
696     // If you're pretty sure you haven't broken anything, change `GENERATE_FIXTURE`
697     // above to `true`, and run `cargo test` to generate a new fixture. Then
698     // **change it back to `false`**. Include the changed `searchindex_fixture.json` in your commit.
699     #[test]
search_index_hasnt_changed_accidentally()700     fn search_index_hasnt_changed_accidentally() {
701         let temp = DummyBook::new().build().unwrap();
702         let md = MDBook::load(temp.path()).unwrap();
703         md.build().unwrap();
704 
705         let book_index = read_book_index(temp.path());
706 
707         let fixture_index = get_fixture();
708 
709         // Uncomment this if you're okay with pretty-printing 32KB of JSON
710         //assert_eq!(fixture_index, book_index);
711 
712         if book_index != fixture_index {
713             panic!("The search index has changed from the fixture");
714         }
715     }
716 }
717