#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::needless_pass_by_value,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions,
clippy::redundant_closure,
clippy::redundant_closure_for_method_calls,
clippy::needless_raw_string_hashes,
clippy::single_char_pattern,
clippy::format_in_format_args,
clippy::needless_late_init,
clippy::if_then_some_else_none,
clippy::must_use_candidate
)]
use anyhow::Result;
use std::fs;
use std::path::Path;
use std::sync::{Arc, Mutex};
use ssg::plugin::{Plugin, PluginContext, PluginManager};
use ssg::postprocess::{HtmlFixPlugin, ManifestFixPlugin};
#[derive(Debug)]
struct TraceTPlugin {
name: &'static str,
log: Arc<Mutex<Vec<(&'static str, &'static str)>>>,
}
impl Plugin for TraceTPlugin {
fn name(&self) -> &'static str {
self.name
}
fn before_compile(&self, _ctx: &PluginContext) -> Result<()> {
self.log.lock().unwrap().push((self.name, "before"));
Ok(())
}
fn after_compile(&self, _ctx: &PluginContext) -> Result<()> {
self.log.lock().unwrap().push((self.name, "after"));
Ok(())
}
fn on_serve(&self, _ctx: &PluginContext) -> Result<()> {
self.log.lock().unwrap().push((self.name, "serve"));
Ok(())
}
}
fn ctx(site: &Path) -> PluginContext {
PluginContext::new(
Path::new("content"),
Path::new("build"),
site,
Path::new("templates"),
)
}
#[test]
fn lifecycle_phases_run_in_documented_order() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut mgr = PluginManager::new();
mgr.register(TraceTPlugin {
name: "a",
log: log.clone(),
});
mgr.register(TraceTPlugin {
name: "b",
log: log.clone(),
});
let tmp = tempfile::tempdir().unwrap();
let c = ctx(tmp.path());
mgr.run_before_compile(&c).unwrap();
mgr.run_after_compile(&c).unwrap();
mgr.run_on_serve(&c).unwrap();
let entries = log.lock().unwrap().clone();
assert_eq!(
entries,
vec![
("a", "before"),
("b", "before"),
("a", "after"),
("b", "after"),
("a", "serve"),
("b", "serve"),
],
"phases must run in registration order within each phase, and \
phases must run before → after → serve"
);
}
#[test]
fn empty_plugin_manager_is_a_no_op() {
let mgr = PluginManager::new();
let tmp = tempfile::tempdir().unwrap();
let c = ctx(tmp.path());
mgr.run_before_compile(&c).unwrap();
mgr.run_after_compile(&c).unwrap();
mgr.run_on_serve(&c).unwrap();
}
#[test]
fn html_fix_plugin_is_idempotent_on_second_run() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path();
let html = r#"<!doctype html><html lang="en"><head>
<meta name="apple-mobile-web-app-capable" content="yes">
<link as=image fetchpriority=high href rel=preload type=image/webp>
<title>x</title>
</head><body><h1>h</h1></body></html>"#;
let c = ctx(site);
let after_first = HtmlFixPlugin
.transform_html(html, Path::new("index.html"), &c)
.unwrap();
let after_second = HtmlFixPlugin
.transform_html(&after_first, Path::new("index.html"), &c)
.unwrap();
assert_eq!(
after_first, after_second,
"HtmlFixPlugin must produce identical output on the second run"
);
assert!(
after_first.contains("name=\"mobile-web-app-capable\""),
"modern meta should be injected"
);
assert!(
!after_first.contains("rel=preload"),
"empty preload should be removed: {after_first}"
);
}
#[test]
fn manifest_fix_plugin_is_idempotent_on_second_run() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path();
fs::write(
site.join("manifest.json"),
r#"{"name":"X","icons":[{"src":"","sizes":"512x512"},{"src":"/i.svg"}]}"#,
)
.unwrap();
let c = ctx(site);
ManifestFixPlugin.after_compile(&c).unwrap();
let after_first = fs::read_to_string(site.join("manifest.json")).unwrap();
ManifestFixPlugin.after_compile(&c).unwrap();
let after_second = fs::read_to_string(site.join("manifest.json")).unwrap();
assert_eq!(
after_first, after_second,
"ManifestFixPlugin must produce identical output on the second run"
);
let v: serde_json::Value = serde_json::from_str(&after_first).unwrap();
let icons = v["icons"].as_array().unwrap();
assert_eq!(icons.len(), 1);
assert_eq!(icons[0]["src"], "/i.svg");
}
#[test]
fn html_fix_runs_before_minify_so_minified_output_is_clean() {
use ssg::plugins::MinifyPlugin;
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path();
fs::write(
site.join("index.html"),
r#"<!doctype html>
<html lang="en">
<head>
<meta name="apple-mobile-web-app-capable" content="yes">
<title>x</title>
</head>
<body><h1>h</h1></body>
</html>
"#,
)
.unwrap();
let mut mgr = PluginManager::new();
mgr.register(HtmlFixPlugin);
mgr.register(MinifyPlugin);
let c = ctx(site);
mgr.run_after_compile(&c).unwrap();
let final_html = fs::read_to_string(site.join("index.html")).unwrap();
assert!(
final_html.contains("mobile-web-app-capable"),
"modern meta should be present in minified output: {final_html}"
);
assert!(
!final_html.contains("\n\n"),
"output should be minified (no blank lines): {final_html}"
);
}
#[test]
fn manifest_fix_plugin_no_op_when_manifest_missing() {
let tmp = tempfile::tempdir().unwrap();
let c = ctx(tmp.path());
ManifestFixPlugin.after_compile(&c).unwrap();
assert!(
!tmp.path().join("manifest.json").exists(),
"plugin should not create a manifest where none exists"
);
}
#[test]
fn html_fix_plugin_no_op_when_site_dir_empty() {
let tmp = tempfile::tempdir().unwrap();
let c = ctx(tmp.path());
HtmlFixPlugin.after_compile(&c).unwrap();
}
#[test]
fn html_fix_plugin_preserves_svg_data_url_imgs() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path();
let svg_url = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'><rect width='10' height='10'/></svg>";
let html = format!(
"<!doctype html><html lang=\"en\"><head><title>x</title></head>\
<body><img src=\"{svg_url}\" alt=\"banner\" width=\"10\" height=\"10\"></body></html>"
);
fs::write(site.join("index.html"), &html).unwrap();
let c = ctx(site);
HtmlFixPlugin.after_compile(&c).unwrap();
let after = fs::read_to_string(site.join("index.html")).unwrap();
assert!(
after.contains(svg_url),
"SVG data URL must be preserved verbatim, got: {after}"
);
assert!(
after.contains("alt=\"banner\""),
"alt attribute must survive past the SVG data URL"
);
}