mod test_manifest;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::Once;
use test_manifest::{
TestCategory, TestResultBuilder, TestSource, extract_pdf_metadata, init_manifest, record_test,
save_manifest,
};
static INIT: Once = Once::new();
fn init_test_manifest() {
INIT.call_once(|| {
let output_dir = project_root().join("target/e2e");
init_manifest(&output_dir, TestSource::Cli);
});
}
fn project_root() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(manifest_dir)
.parent() .unwrap()
.parent() .unwrap()
.to_path_buf()
}
fn binary_path() -> PathBuf {
let root = project_root();
let debug_path = root.join("target/debug/printwell");
let release_path = root.join("target/release/printwell");
if release_path.exists() {
release_path
} else if debug_path.exists() {
debug_path
} else {
panic!("Binary not found. Run: cargo build -p printwell-cli");
}
}
fn native_lib_dir() -> PathBuf {
let root = project_root();
let target_native = root.join("target/native");
let prebuilt = root.join("native/linux-x64");
if target_native.join("libprintwell_native.so").exists() {
target_native
} else if prebuilt.join("libprintwell_native.so").exists() {
prebuilt
} else {
panic!("Native library not found. Run: cargo xtask build");
}
}
fn run_printwell(args: &[&str]) -> Output {
let binary = binary_path();
let native_dir = native_lib_dir();
let root = project_root();
let existing_ld_path = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
let ld_path = format!("{}:{}", native_dir.display(), existing_ld_path);
Command::new(&binary)
.args(args)
.current_dir(&root)
.env("LD_LIBRARY_PATH", &ld_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run printwell: {}", e))
}
fn ensure_output_dir() {
let root = project_root();
let output_dir = root.join("target/e2e");
std::fs::create_dir_all(&output_dir).expect("Failed to create output directory");
}
fn output_path(name: &str) -> PathBuf {
project_root().join(format!("target/e2e/{}.pdf", name))
}
fn run_test_with_manifest<F>(
name: &str,
category: TestCategory,
description: &str,
input: Option<&str>,
expected_orientation: Option<&str>,
test_fn: F,
) where
F: FnOnce() -> (PathBuf, Result<(), String>),
{
init_test_manifest();
ensure_output_dir();
let mut result = TestResultBuilder::new(name, category)
.description(description)
.start_timer();
if let Some(input_path) = input {
if input_path.starts_with("http") {
result = result.input_url(input_path);
} else if input_path.ends_with(".pdf") {
result = result.input_pdf_file(input_path);
} else {
result = result.input_html_file(input_path);
}
}
if let Some(orient) = expected_orientation {
result = result.expect_orientation(orient);
}
let (pdf_path, test_result) = test_fn();
result = result.output_path(&pdf_path.to_string_lossy());
match test_result {
Ok(()) => {
if let Some(actual) = extract_pdf_metadata(&pdf_path) {
result = result.actual_properties(
actual.orientation.as_deref().unwrap_or("unknown"),
actual.page_count.unwrap_or(0),
actual.width_pt.unwrap_or(0.0),
actual.height_pt.unwrap_or(0.0),
);
}
result = result.pass();
}
Err(error) => {
result = result.fail(&error);
}
}
record_test(result.stop_timer().build());
save_manifest();
}
#[test]
fn test_basic_conversion() {
run_test_with_manifest(
"basic_conversion",
TestCategory::Conversion,
"Simple HTML to PDF conversion",
Some("tests/fixtures/test_basic.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_basic.html",
"-o",
"target/e2e/test_basic.pdf",
]);
let pdf_path = output_path("test_basic");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_basic: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_custom_font() {
run_test_with_manifest(
"custom_font",
TestCategory::Fonts,
"Custom font loading",
Some("tests/fixtures/test_custom_font.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_custom_font.html",
"-o",
"target/e2e/test_custom_font.pdf",
]);
let pdf_path = output_path("test_custom_font");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_custom_font: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_multilang() {
run_test_with_manifest(
"multilang",
TestCategory::Conversion,
"Unicode and multilingual content",
Some("tests/fixtures/test_multilang.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_multilang.html",
"-o",
"target/e2e/test_multilang.pdf",
]);
let pdf_path = output_path("test_multilang");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_multilang: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_multipage() {
run_test_with_manifest(
"multipage",
TestCategory::Conversion,
"Multi-page document generation",
Some("tests/fixtures/test_multipage.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_multipage.html",
"-o",
"target/e2e/test_multipage.pdf",
]);
let pdf_path = output_path("test_multipage");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_multipage: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_landscape_basic() {
run_test_with_manifest(
"landscape_basic",
TestCategory::Orientation,
"Basic landscape orientation",
Some("tests/fixtures/test_landscape.html"),
Some("landscape"),
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_landscape.html",
"--landscape",
"-o",
"target/e2e/test_landscape_basic.pdf",
]);
let pdf_path = output_path("test_landscape_basic");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_landscape_basic: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_landscape_multipage() {
run_test_with_manifest(
"landscape_multipage",
TestCategory::Orientation,
"Multi-page landscape document",
Some("tests/fixtures/test_multipage.html"),
Some("landscape"),
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_multipage.html",
"--landscape",
"-o",
"target/e2e/test_landscape_multipage.pdf",
]);
let pdf_path = output_path("test_landscape_multipage");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_landscape_multipage: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_batch_conversion() {
run_test_with_manifest(
"batch_conversion",
TestCategory::Batch,
"Batch conversion with pool",
Some("tests/fixtures/test_basic.html"),
None,
|| {
let root = project_root();
let batch_dir = root.join("target/e2e/batch");
std::fs::create_dir_all(&batch_dir).expect("Failed to create batch directory");
let output = run_printwell(&[
"convert-batch",
"tests/fixtures/test_basic.html",
"-o",
"target/e2e/batch",
"--workers",
"2",
]);
let pdf_path = batch_dir.join("test_basic.pdf");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Batch output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Batch output PDF is empty".to_string()));
}
println!("test_batch: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_boundaries() {
run_test_with_manifest(
"boundaries",
TestCategory::Conversion,
"Page boundary handling",
Some("tests/fixtures/test_forms.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_forms.html",
"-o",
"target/e2e/test_boundaries.pdf",
"--boundaries",
"input,select,textarea",
"--boundaries-output",
"target/e2e/test_boundaries.json",
]);
let pdf_path = output_path("test_boundaries");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
let json_path = project_root().join("target/e2e/test_boundaries.json");
if !json_path.exists() {
return (pdf_path, Err("Boundaries JSON not created".to_string()));
}
let content =
std::fs::read_to_string(&json_path).expect("Failed to read boundaries JSON");
let count = content.matches("selector").count();
if count == 0 {
return (pdf_path, Err("No boundaries extracted".to_string()));
}
println!("test_boundaries: {} boundaries extracted", count);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_remote_image() {
run_test_with_manifest(
"remote_image",
TestCategory::Images,
"Remote image loading",
Some("tests/fixtures/test_remote_image.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_remote_image.html",
"-o",
"target/e2e/test_remote_image.pdf",
]);
let pdf_path = output_path("test_remote_image");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_remote_image: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_url_rendering() {
run_test_with_manifest(
"url_rendering",
TestCategory::Url,
"URL to PDF conversion",
Some("https://example.com"),
None,
|| {
let output = run_printwell(&[
"convert",
"https://example.com",
"-o",
"target/e2e/test_url_rendering.pdf",
]);
let pdf_path = project_root().join("target/e2e/test_url_rendering.pdf");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_url_rendering: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_print_background() {
run_test_with_manifest(
"print_background",
TestCategory::Conversion,
"Background printing enabled",
Some("tests/fixtures/test_background.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_background.html",
"-o",
"target/e2e/test_background.pdf",
]);
let pdf_path = output_path("test_background");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_print_background: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_header_footer() {
run_test_with_manifest(
"header_footer",
TestCategory::Conversion,
"Header and footer templates",
Some("tests/fixtures/test_basic.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_basic.html",
"-o",
"target/e2e/test_header_footer.pdf",
"--header",
"<div style='font-size:10px;text-align:center;width:100%'>Header - Test Document</div>",
"--footer",
"<div style='font-size:10px;text-align:center;width:100%'>Page <span class='pageNumber'></span></div>",
"--margin",
"50mm,20mm,50mm,20mm",
]);
let pdf_path = output_path("test_header_footer");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_header_footer: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_scale() {
run_test_with_manifest(
"scale",
TestCategory::Conversion,
"Scale option",
Some("tests/fixtures/test_basic.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_basic.html",
"-o",
"target/e2e/test_scale.pdf",
"--scale",
"0.5",
]);
let pdf_path = output_path("test_scale");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_scale: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_pdf_metadata() {
run_test_with_manifest(
"pdf_metadata",
TestCategory::Metadata,
"PDF with custom metadata",
Some("tests/fixtures/test_basic.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_basic.html",
"-o",
"target/e2e/test_metadata.pdf",
"--title",
"Test Document",
"--author",
"printwell CLI Tests",
"--subject",
"E2E Testing",
"--keywords",
"test,printwell,pdf",
]);
let pdf_path = output_path("test_metadata");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_pdf_metadata: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_embedded_image() {
run_test_with_manifest(
"embedded_image",
TestCategory::Images,
"Embedded base64 image",
Some("tests/fixtures/test_embedded_image.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_embedded_image.html",
"-o",
"target/e2e/test_embedded_image.pdf",
]);
let pdf_path = output_path("test_embedded_image");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_embedded_image: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_html_tables() {
run_test_with_manifest(
"html_tables",
TestCategory::Conversion,
"HTML tables rendering",
Some("tests/fixtures/test_tables.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_tables.html",
"-o",
"target/e2e/test_tables.pdf",
]);
let pdf_path = output_path("test_tables");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_html_tables: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_html_forms() {
run_test_with_manifest(
"html_forms",
TestCategory::Forms,
"HTML form elements rendering",
Some("tests/fixtures/test_forms.html"),
None,
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_forms.html",
"-o",
"target/e2e/test_html_forms.pdf",
]);
let pdf_path = output_path("test_html_forms");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_html_forms: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_css_page_size() {
run_test_with_manifest(
"css_page_size",
TestCategory::Orientation,
"CSS @page size preference",
Some("tests/fixtures/test_css_page_size.html"),
Some("landscape"),
|| {
let output = run_printwell(&[
"convert",
"tests/fixtures/test_css_page_size.html",
"-o",
"target/e2e/test_css_page_size.pdf",
]);
let pdf_path = output_path("test_css_page_size");
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
if !pdf_path.exists() {
return (pdf_path, Err("Output PDF not created".to_string()));
}
let size = std::fs::metadata(&pdf_path).unwrap().len();
if size == 0 {
return (pdf_path, Err("Output PDF is empty".to_string()));
}
println!("test_css_page_size: {} bytes", size);
(pdf_path, Ok(()))
},
);
}
#[test]
fn test_renderer_info() {
run_test_with_manifest(
"renderer_info",
TestCategory::Conversion,
"Verify renderer info available",
None,
None,
|| {
let output = run_printwell(&["info"]);
let pdf_path = PathBuf::new();
if !output.status.success() {
return (
pdf_path,
Err(String::from_utf8_lossy(&output.stderr).to_string()),
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.contains("Printwell") || !stdout.contains("Chromium") {
return (
pdf_path,
Err("Info output missing version information".to_string()),
);
}
println!("renderer_info: {}", stdout.trim());
(pdf_path, Ok(()))
},
);
}