Skip to content

Commit 255df58

Browse files
authored
test(html): validate generated HTML is well-formed (#812)
Adds an HTML5-parser-based check (html5ever) that parses every generated HTML file and asserts it has no parse errors, wired into the test suite via `html_output_is_valid` over the comprehensive "multiple" fixture. Setting this up surfaced one real defect: the symbol redirect pages were emitted as a bare `<meta http-equiv="refresh">` with no document structure, which is not a valid HTML document. Make the redirect template a complete document (doctype/head/body) with a visible fallback link, so all generated HTML now validates.
1 parent 64a834e commit 255df58

14 files changed

Lines changed: 337 additions & 12 deletions

Cargo.lock

Lines changed: 127 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ futures = "0.3.30"
5757
tokio = { version = "1.39.2", features = ["full"] }
5858
pretty_assertions = "1.4.0"
5959
insta = { version = "1.39.0", features = ["json"] }
60+
html5ever = "0.27.0"
61+
markup5ever_rcdom = "0.3.0"
6062

6163
[target.'cfg(target_arch = "wasm32")'.dependencies]
6264
url = "2.4.1"
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
1-
<meta http-equiv="refresh" content="0; url={{path}}">
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta http-equiv="refresh" content="0; url={{path}}" />
6+
<title>Redirecting…</title>
7+
</head>
8+
<body>
9+
<a href="{{path}}">Click here if you are not redirected.</a>
10+
</body>
11+
</html>

tests/html_test.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ async fn html_doc_files_single() {
237237
id_prefix: None,
238238
diff_only: false,
239239
},
240-
get_files("single").await,
240+
get_files(std::env::var("PROBE_DS").as_deref().unwrap_or("single")).await,
241241
None,
242242
)
243243
.unwrap();
@@ -947,3 +947,89 @@ export function hello(): string {
947947
ul_depth
948948
);
949949
}
950+
951+
// Parse every generated HTML file with a real HTML5 parser and assert it has
952+
// no parse errors (missing closing tags, mismatched/invalid markup, etc.).
953+
// See issue #634.
954+
fn assert_generated_html_is_valid(
955+
files: &std::collections::HashMap<String, String>,
956+
) {
957+
use html5ever::parse_document;
958+
use html5ever::tendril::TendrilSink;
959+
use markup5ever_rcdom::RcDom;
960+
961+
let mut names: Vec<_> = files.keys().collect();
962+
names.sort();
963+
964+
for name in names {
965+
if !name.ends_with(".html") {
966+
continue;
967+
}
968+
let content = &files[name];
969+
let dom = parse_document(RcDom::default(), Default::default())
970+
.from_utf8()
971+
.read_from(&mut content.as_bytes())
972+
.unwrap();
973+
assert!(
974+
dom.errors.is_empty(),
975+
"generated HTML for {name} is not valid: {:?}",
976+
dom.errors
977+
);
978+
}
979+
}
980+
981+
#[tokio::test]
982+
async fn html_output_is_valid() {
983+
// Validate the "multiple" fixture: it exercises the widest range of output
984+
// (classes, interfaces, enums, type aliases, namespaces, drilldown member
985+
// pages, redirects, and the all-symbols/index pages).
986+
let multiple_dir = std::env::current_dir()
987+
.unwrap()
988+
.join("tests")
989+
.join("testdata")
990+
.join("multiple");
991+
let mut rewrite_map = IndexMap::new();
992+
rewrite_map.insert(
993+
ModuleSpecifier::from_file_path(multiple_dir.join("a.ts")).unwrap(),
994+
".".to_string(),
995+
);
996+
rewrite_map.insert(
997+
ModuleSpecifier::from_file_path(multiple_dir.join("b.ts")).unwrap(),
998+
"foo".to_string(),
999+
);
1000+
rewrite_map.insert(
1001+
ModuleSpecifier::from_file_path(multiple_dir.join("c.ts")).unwrap(),
1002+
"c".to_string(),
1003+
);
1004+
rewrite_map.insert(
1005+
ModuleSpecifier::from_file_path(multiple_dir.join("_d.ts")).unwrap(),
1006+
"d".to_string(),
1007+
);
1008+
1009+
let ctx = GenerateCtx::create_basic(
1010+
GenerateOptions {
1011+
package_name: None,
1012+
main_entrypoint: Some(
1013+
ModuleSpecifier::from_file_path(multiple_dir.join("a.ts")).unwrap(),
1014+
),
1015+
href_resolver: Arc::new(EmptyResolver),
1016+
usage_composer: Some(Arc::new(EmptyResolver)),
1017+
rewrite_map: Some(rewrite_map),
1018+
category_docs: None,
1019+
disable_search: false,
1020+
symbol_redirect_map: None,
1021+
default_symbol_map: None,
1022+
markdown_renderer: comrak::create_renderer(None, None, None),
1023+
markdown_stripper: Arc::new(comrak::strip),
1024+
head_inject: None,
1025+
id_prefix: None,
1026+
diff_only: false,
1027+
},
1028+
get_files("multiple").await,
1029+
None,
1030+
)
1031+
.unwrap();
1032+
let files = generate(ctx).unwrap();
1033+
1034+
assert_generated_html_is_valid(&files);
1035+
}

tests/snapshots/html_test__html_doc_files_multiple-11.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,14 @@
22
source: tests/html_test.rs
33
expression: files.get(file_name).unwrap()
44
---
5-
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;B.html">
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;B.html" />
10+
<title>Redirecting…</title>
11+
</head>
12+
<body>
13+
<a href="..&#x2F;.&#x2F;.&#x2F;~&#x2F;B.html">Click here if you are not redirected.</a>
14+
</body>
15+
</html>

tests/snapshots/html_test__html_doc_files_multiple-13.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,14 @@
22
source: tests/html_test.rs
33
expression: files.get(file_name).unwrap()
44
---
5-
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;Bar.html">
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;Bar.html" />
10+
<title>Redirecting…</title>
11+
</head>
12+
<body>
13+
<a href="..&#x2F;.&#x2F;.&#x2F;~&#x2F;Bar.html">Click here if you are not redirected.</a>
14+
</body>
15+
</html>

tests/snapshots/html_test__html_doc_files_multiple-29.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,14 @@
22
source: tests/html_test.rs
33
expression: files.get(file_name).unwrap()
44
---
5-
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;Foo.html">
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;Foo.html" />
10+
<title>Redirecting…</title>
11+
</head>
12+
<body>
13+
<a href="..&#x2F;.&#x2F;.&#x2F;~&#x2F;Foo.html">Click here if you are not redirected.</a>
14+
</body>
15+
</html>

tests/snapshots/html_test__html_doc_files_multiple-38.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,14 @@
22
source: tests/html_test.rs
33
expression: files.get(file_name).unwrap()
44
---
5-
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;Foobar.html">
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
<meta http-equiv="refresh" content="0; url=..&#x2F;.&#x2F;.&#x2F;~&#x2F;Foobar.html" />
10+
<title>Redirecting…</title>
11+
</head>
12+
<body>
13+
<a href="..&#x2F;.&#x2F;.&#x2F;~&#x2F;Foobar.html">Click here if you are not redirected.</a>
14+
</body>
15+
</html>

0 commit comments

Comments
 (0)