Files
FoFreeXit/crates/ff-engine/tests/form_roundtrip.rs
T

191 lines
6.9 KiB
Rust

//! Test Form AcroForm (Phase 6): tạo field → liệt kê → điền → export/import
//! FDF & CSV → flatten. Cần binary `qpdf` để chuẩn hoá fixture cho lopdf.
use std::path::PathBuf;
use ff_engine::{FieldKind, FieldValue, NewField};
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("workspace root")
}
// KHÔNG đè biến môi trường nếu đã có (CI set sẵn); mặc định thử qpdf/ ở root
// workspace (fetch-qpdf.ps1 đặt ở đó), fallback /usr/bin cho máy Linux.
fn ensure_qpdf_path() {
if std::env::var("FOFREEXIT_QPDF_PATH").is_err() {
let ws = workspace_root().join("qpdf").join("bin");
if ws.exists() {
std::env::set_var("FOFREEXIT_QPDF_PATH", ws);
} else {
std::env::set_var("FOFREEXIT_QPDF_PATH", "/usr/bin");
}
}
}
fn pdfium() -> pdfium_render::prelude::Pdfium {
if std::env::var("FOFREEXIT_PDFIUM_PATH").is_err() {
std::env::set_var("FOFREEXIT_PDFIUM_PATH", workspace_root());
}
ensure_qpdf_path();
ff_engine::bind_pdfium().expect("nạp PDFium")
}
fn tmp(name: &str) -> PathBuf {
let p = std::env::temp_dir().join(name);
let _ = std::fs::remove_file(&p);
p
}
/// Dựng fixture form: chuẩn hoá sample rồi thêm 1 text + 1 checkbox + 1 combo.
fn make_form_fixture(out: &std::path::Path) {
ensure_qpdf_path();
let norm = tmp("ff_form_norm.pdf");
ff_engine::repair(&workspace_root().join("corpus").join("sample-multipage.pdf"), &norm)
.expect("normalize");
let fields = vec![
NewField {
name: "hoTen".into(),
kind: FieldKind::Text,
page_index: 0,
rect: [80.0, 700.0, 320.0, 720.0],
value: String::new(),
options: vec![],
},
NewField {
name: "dongY".into(),
kind: FieldKind::Checkbox,
page_index: 0,
rect: [80.0, 660.0, 96.0, 676.0],
value: "off".into(),
options: vec![],
},
NewField {
name: "gioiTinh".into(),
kind: FieldKind::Combo,
page_index: 0,
rect: [80.0, 620.0, 240.0, 640.0],
value: String::new(),
options: vec!["Nam".into(), "Nữ".into(), "Khác".into()],
},
];
ff_engine::create_form_fields(&norm, &fields, out).expect("create fields");
}
#[test]
fn create_and_list_fields() {
let _ = pdfium();
let fx = tmp("ff_form_fx.pdf");
make_form_fixture(&fx);
let fields = ff_engine::list_form_fields(&fx).expect("list");
assert_eq!(fields.len(), 3, "phải có 3 field: {:?}", fields.iter().map(|f| &f.name).collect::<Vec<_>>());
let by_name = |n: &str| fields.iter().find(|f| f.name == n).unwrap_or_else(|| panic!("thiếu {n}"));
assert_eq!(by_name("hoTen").kind, FieldKind::Text);
assert_eq!(by_name("dongY").kind, FieldKind::Checkbox);
let combo = by_name("gioiTinh");
assert_eq!(combo.kind, FieldKind::Combo);
assert_eq!(combo.options, vec!["Nam".to_string(), "Nữ".into(), "Khác".into()]);
// Field nằm đúng trang 0 và có rect.
assert_eq!(by_name("hoTen").page_index, Some(0));
assert!(by_name("hoTen").rect.is_some());
}
#[test]
fn fill_text_checkbox_combo_round_trips() {
let _ = pdfium();
let fx = tmp("ff_form_fill_fx.pdf");
let out = tmp("ff_form_filled.pdf");
make_form_fixture(&fx);
let values = vec![
FieldValue { name: "hoTen".into(), value: "Nguyễn Văn A".into() },
FieldValue { name: "dongY".into(), value: "on".into() },
FieldValue { name: "gioiTinh".into(), value: "Nữ".into() },
];
let n = ff_engine::fill_form_fields(&fx, &values, &out).expect("fill");
assert_eq!(n, 3, "phải điền 3 field");
let fields = ff_engine::list_form_fields(&out).expect("list out");
let get = |n: &str| fields.iter().find(|f| f.name == n).unwrap();
assert_eq!(get("hoTen").value.as_deref(), Some("Nguyễn Văn A"), "text (tiếng Việt) round-trip");
assert_eq!(get("gioiTinh").value.as_deref(), Some("Nữ"));
// Checkbox bật → /V là tên on-state (khác "Off").
let cb = get("dongY").value.clone().unwrap_or_default();
assert!(cb != "Off" && !cb.is_empty(), "checkbox bật phải có on-state, được {cb:?}");
}
#[test]
fn fdf_export_import_round_trip() {
let _ = pdfium();
let fx = tmp("ff_form_fdf_fx.pdf");
let filled = tmp("ff_form_fdf_filled.pdf");
let fdf = tmp("ff_form_out.fdf");
let reimported = tmp("ff_form_reimport.pdf");
make_form_fixture(&fx);
let values = vec![
FieldValue { name: "hoTen".into(), value: "Trần Thị B".into() },
FieldValue { name: "gioiTinh".into(), value: "Khác".into() },
];
ff_engine::fill_form_fields(&fx, &values, &filled).expect("fill");
ff_engine::export_fdf(&filled, &fdf).expect("export fdf");
// FDF chứa tên + giá trị.
let parsed = ff_engine::parse_fdf(&fdf).expect("parse fdf");
let hoten = parsed.iter().find(|v| v.name == "hoTen").expect("hoTen trong fdf");
assert_eq!(hoten.value, "Trần Thị B");
// Import FDF vào fixture rỗng → giá trị được điền lại.
let n = ff_engine::import_fdf(&fx, &fdf, &reimported).expect("import fdf");
assert!(n >= 2, "import phải điền ≥2 field");
let fields = ff_engine::list_form_fields(&reimported).expect("list reimport");
assert_eq!(
fields.iter().find(|f| f.name == "hoTen").unwrap().value.as_deref(),
Some("Trần Thị B")
);
}
#[test]
fn export_csv_has_rows() {
let _ = pdfium();
let fx = tmp("ff_form_csv_fx.pdf");
let filled = tmp("ff_form_csv_filled.pdf");
let csv = tmp("ff_form_out.csv");
make_form_fixture(&fx);
ff_engine::fill_form_fields(
&fx,
&[FieldValue { name: "hoTen".into(), value: "CSV Test".into() }],
&filled,
)
.expect("fill");
ff_engine::export_csv(&filled, &csv).expect("export csv");
let text = std::fs::read_to_string(&csv).expect("đọc csv");
assert!(text.starts_with("name,value"), "CSV phải có header");
assert!(text.contains("hoTen,CSV Test"), "CSV phải chứa giá trị: {text}");
}
#[test]
fn flatten_removes_interactive_fields() {
let pdf = pdfium();
let fx = tmp("ff_form_flat_fx.pdf");
let filled = tmp("ff_form_flat_filled.pdf");
let flat = tmp("ff_form_flat.pdf");
make_form_fixture(&fx);
ff_engine::fill_form_fields(
&fx,
&[FieldValue { name: "hoTen".into(), value: "Flatten Me".into() }],
&filled,
)
.expect("fill");
ff_engine::flatten_form(&pdf, &filled, &flat, None).expect("flatten");
// Sau flatten không còn field tương tác.
let fields = ff_engine::list_form_fields(&flat).expect("list flat");
assert!(fields.is_empty(), "flatten phải bỏ hết field tương tác, còn: {:?}", fields.iter().map(|f| &f.name).collect::<Vec<_>>());
}