use std::fs;
use std::time::Duration;
use mini_static::{CssOptions, CssTool, JsOptions, JsTool, Server};
use tempfile::TempDir;
#[tokio::test]
#[ignore = "requires lightningcss on PATH"]
async fn css_bundle_and_minify_produces_minified_concatenated_output() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::write(
src.path().join("reset.css"),
"* {\n margin: 0;\n padding: 0;\n}\n",
)
.unwrap();
fs::write(
src.path().join("theme.css"),
"body {\n background: white;\n}\n",
)
.unwrap();
let server = Server::new(out.path())
.unwrap()
.with_source_folder(src.path())
.unwrap()
.with_css_tool(
CssTool::LightningCss,
CssOptions::new().bundle(true).minify(true),
);
let (_port, handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
let content = fs::read_to_string(out.path().join("styles.css")).unwrap();
assert!(content.contains("margin"), "expected reset CSS in bundle");
assert!(
content.contains("background"),
"expected theme CSS in bundle"
);
assert!(
!content.contains('\n'),
"minified output should not contain source formatting newlines, got: {content}"
);
handle.shutdown().await;
}
#[tokio::test]
#[ignore = "requires lightningcss on PATH"]
async fn prune_output_removes_stale_css_bundle_when_no_css_sources_remain() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::write(out.path().join("styles.css"), "/* stale */").unwrap();
let server = Server::new(out.path())
.unwrap()
.with_source_folder(src.path())
.unwrap()
.with_css_tool(CssTool::LightningCss, CssOptions::new().bundle(true))
.with_prune_output();
let (_port, handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
!out.path().join("styles.css").exists(),
"prune is opt-in: with_prune_output should delete the stale bundle produced by no css sources"
);
handle.shutdown().await;
}
#[tokio::test]
#[ignore = "requires esbuild on PATH"]
async fn js_bundle_and_minify_resolves_the_entrys_module_graph() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::write(
src.path().join("util.js"),
"export function greet(name) { return 'hello ' + name; }\n",
)
.unwrap();
let entry = src.path().join("main.js");
fs::write(
&entry,
"import { greet } from './util.js';\nconsole.log(greet('world'));\n",
)
.unwrap();
let server = Server::new(out.path())
.unwrap()
.with_source_folder(src.path())
.unwrap()
.with_js_tool(
JsTool::Esbuild,
JsOptions::new()
.bundle_entry(&entry, "bundle.js")
.minify(true),
)
.unwrap();
let (_port, handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
let content = fs::read_to_string(out.path().join("bundle.js")).unwrap();
assert!(
content.contains("hello"),
"bundle should inline the imported module's code, got: {content}"
);
assert!(
!content.contains("import "),
"a bundled entry should have no remaining import statements, got: {content}"
);
handle.shutdown().await;
}
#[tokio::test]
#[ignore = "requires esbuild on PATH"]
async fn malformed_js_source_degrades_to_raw_output() {
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
let malformed = b"function( {{{ !!!";
fs::write(src.path().join("broken.js"), malformed).unwrap();
let server = Server::new(out.path())
.unwrap()
.with_source_folder(src.path())
.unwrap()
.with_js_tool(JsTool::Esbuild, JsOptions::new().minify(true))
.unwrap();
let (_port, handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
let out_js = fs::read(out.path().join("broken.js")).unwrap();
assert_eq!(
out_js, malformed,
"a file esbuild cannot parse must still be mirrored (raw) so the page stays in sync"
);
handle.shutdown().await;
}