use mini_static::{Server, SpaTransition};
use std::fs;
use tempfile::TempDir;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
async fn get(addr: &str, path: &str) -> String {
let mut conn = TcpStream::connect(addr).await.unwrap();
conn.write_all(
format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n").as_bytes(),
)
.await
.unwrap();
let mut response_data = Vec::new();
conn.read_to_end(&mut response_data).await.unwrap();
String::from_utf8_lossy(&response_data).into_owned()
}
const FIXTURE_HTML: &[u8] = b"<html><body><h1>hi</h1></body></html>";
#[tokio::test]
async fn spa_mode_disabled_by_default_serves_html_unmodified() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let server = Server::new(root.path()).unwrap();
let (port, handle) = server.run_ephemeral().await.unwrap();
let addr = format!("127.0.0.1:{port}");
let response = get(&addr, "/index.html").await;
assert!(response.contains("HTTP/1.1 200"));
assert!(
!response.contains("<script>"),
"no spa script should be injected when spa-mode was never enabled, got: {response}"
);
assert!(
response.ends_with(std::str::from_utf8(FIXTURE_HTML).unwrap()),
"body should be byte-identical to the fixture, got: {response}"
);
handle.shutdown().await;
}
#[tokio::test]
async fn with_spa_mode_injects_a_script_targeting_document_body() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let server = Server::new(root.path()).unwrap().with_spa_mode();
let (port, handle) = server.run_ephemeral().await.unwrap();
let addr = format!("127.0.0.1:{port}");
let response = get(&addr, "/index.html").await;
assert!(response.contains("mini-static:navigate"), "got: {response}");
assert!(response.contains("doc.body"), "got: {response}");
assert!(response.contains("ROOT_SELECTOR=null;"), "got: {response}");
handle.shutdown().await;
}
#[tokio::test]
async fn with_spa_root_injects_a_script_targeting_the_configured_selector() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let server = Server::new(root.path()).unwrap().with_spa_root("#app");
let (port, handle) = server.run_ephemeral().await.unwrap();
let addr = format!("127.0.0.1:{port}");
let response = get(&addr, "/index.html").await;
assert!(
response.contains("ROOT_SELECTOR=\"#app\";"),
"got: {response}"
);
assert!(
!response.contains("ROOT_SELECTOR=null;"),
"configuring a root selector should not leave the null-body fallback, got: {response}"
);
handle.shutdown().await;
}
#[tokio::test]
async fn spa_mode_and_root_builders_compose_regardless_of_call_order() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let root_then_mode = Server::new(root.path())
.unwrap()
.with_spa_root("#app")
.with_spa_mode();
let (port_a, handle_a) = root_then_mode.run_ephemeral().await.unwrap();
let addr_a = format!("127.0.0.1:{port_a}");
let response_a = get(&addr_a, "/index.html").await;
assert!(
response_a.contains("ROOT_SELECTOR=\"#app\";"),
"with_spa_root().with_spa_mode() should keep the configured selector, got: {response_a}"
);
handle_a.shutdown().await;
let mode_then_root = Server::new(root.path())
.unwrap()
.with_spa_mode()
.with_spa_root("#app");
let (port_b, handle_b) = mode_then_root.run_ephemeral().await.unwrap();
let addr_b = format!("127.0.0.1:{port_b}");
let response_b = get(&addr_b, "/index.html").await;
assert!(
response_b.contains("ROOT_SELECTOR=\"#app\";"),
"with_spa_mode().with_spa_root() should set the selector, got: {response_b}"
);
handle_b.shutdown().await;
}
#[tokio::test]
async fn default_transition_injects_no_style_tag() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let server = Server::new(root.path()).unwrap().with_spa_mode();
let (port, handle) = server.run_ephemeral().await.unwrap();
let addr = format!("127.0.0.1:{port}");
let response = get(&addr, "/index.html").await;
assert!(
!response.contains("<style>"),
"fade is the default and injects no CSS, got: {response}"
);
handle.shutdown().await;
}
#[tokio::test]
async fn slide_transition_injects_a_style_tag_before_the_script() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let server = Server::new(root.path())
.unwrap()
.with_spa_root("#app")
.with_spa_transition(SpaTransition::Slide);
let (port, handle) = server.run_ephemeral().await.unwrap();
let addr = format!("127.0.0.1:{port}");
let response = get(&addr, "/index.html").await;
assert!(
response.contains("mix-blend-mode:normal"),
"got: {response}"
);
let style_pos = response.find("<style>").expect("style tag present");
let script_pos = response.find("<script>").expect("script tag present");
assert!(
style_pos < script_pos,
"style tag should precede the script tag, got: {response}"
);
handle.shutdown().await;
}
#[tokio::test]
async fn with_spa_transition_alone_enables_spa_mode() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let server = Server::new(root.path())
.unwrap()
.with_spa_transition(SpaTransition::Slide);
let (port, handle) = server.run_ephemeral().await.unwrap();
let addr = format!("127.0.0.1:{port}");
let response = get(&addr, "/index.html").await;
assert!(response.contains("mini-static:navigate"), "got: {response}");
handle.shutdown().await;
}
#[tokio::test]
async fn spa_mode_composes_with_live_reload_both_scripts_present() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), FIXTURE_HTML).unwrap();
let server = Server::new(root.path())
.unwrap()
.with_spa_mode()
.with_live_reload();
let (port, handle) = server.run_ephemeral().await.unwrap();
let addr = format!("127.0.0.1:{port}");
let response = get(&addr, "/index.html").await;
assert!(
response.contains("mini-static:navigate"),
"spa script missing, got: {response}"
);
assert!(
response.contains("EventSource"),
"reload script missing, got: {response}"
);
let body_start = response.find("<html>").expect("html body present");
let body_end = response.rfind("</body>").expect("closing body tag present");
let spa_pos = response.find("mini-static:navigate").unwrap();
let reload_pos = response.find("EventSource").unwrap();
assert!(
(body_start..body_end).contains(&spa_pos),
"spa script must land before </body>, got: {response}"
);
assert!(
(body_start..body_end).contains(&reload_pos),
"reload script must land before </body>, got: {response}"
);
handle.shutdown().await;
}