4806209761
Whiteboard: zoom bar and dot grid, layers panel, appear motion, sound cues, focus rings and AccessKit labels; .draw files with legacy .excalidraw still readable; notescraft-cli export whiteboard. Security: export.pdf and file.export_whiteboard_svg accept vault-relative out paths only (they are reachable by the assistant and the control channel). The CLI places absolute --out files itself via the new notescraft_engine::render_pdf; the UI export worker renders and the live session writes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
98 lines
4.7 KiB
Rust
98 lines
4.7 KiB
Rust
//! `notescraft-cli export pdf` (SPEC 10.1, SPEC 16): the PDF export
|
|
//! subcommand, its options and its honest `--print` report.
|
|
|
|
mod common;
|
|
|
|
use common::{empty_vault, note, run};
|
|
use serde_json::Value;
|
|
|
|
/// A vault with one note ready to export.
|
|
fn fixture(name: &str) -> std::path::PathBuf {
|
|
let root = empty_vault(name);
|
|
note(&root, "Report.md", "# Report\n\nBody with **bold** and a [link](https://example.com).\n\n- [x] done\n");
|
|
root
|
|
}
|
|
|
|
#[test]
|
|
fn export_pdf_writes_the_file_and_reports_it() {
|
|
let vault = fixture("export-text");
|
|
let run = run(&vault, &["export", "pdf", "Report.md"]);
|
|
assert_eq!(run.code, 0, "{}", run.stderr);
|
|
assert!(run.stdout.contains("wrote Report.pdf"), "{}", run.stdout);
|
|
assert!(run.stdout.contains("pages: 1"), "{}", run.stdout);
|
|
let bytes = std::fs::read(vault.join("Report.pdf")).expect("the PDF was written");
|
|
assert!(bytes.starts_with(b"%PDF-1.7\n"), "a real PDF landed in the vault");
|
|
assert!(bytes.ends_with(b"%%EOF\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn export_pdf_reports_its_result_as_json() {
|
|
let vault = fixture("export-json");
|
|
let run = run(&vault, &["--json", "export", "pdf", "Report.md"]);
|
|
assert_eq!(run.code, 0, "{}", run.stderr);
|
|
let result: Value = run.result();
|
|
assert_eq!(result.get("path").and_then(Value::as_str), Some("Report.pdf"));
|
|
assert_eq!(result.get("pages").and_then(Value::as_u64), Some(1));
|
|
assert!(result.get("bytes").and_then(Value::as_u64).unwrap_or_default() > 1_000, "{result}");
|
|
}
|
|
|
|
#[test]
|
|
fn out_chooses_the_destination_and_may_leave_the_vault() {
|
|
let vault = fixture("export-out");
|
|
let inside = run(&vault, &["--json", "export", "pdf", "Report.md", "--out", "reports/report.pdf"]);
|
|
assert_eq!(inside.code, 0, "{}", inside.stderr);
|
|
assert!(vault.join("reports/report.pdf").exists(), "a relative --out stays in the vault");
|
|
|
|
let outside = vault.join("outside.pdf");
|
|
let absolute = run(&vault, &["--json", "export", "pdf", "Report.md", "--out", outside.display().to_string().as_str()]);
|
|
assert_eq!(absolute.code, 0, "{}", absolute.stderr);
|
|
assert!(outside.exists(), "an absolute --out is the one way out of the vault");
|
|
}
|
|
|
|
#[test]
|
|
fn page_and_margin_options_reach_the_engine() {
|
|
let vault = fixture("export-options");
|
|
let letter = run(&vault, &["--json", "export", "pdf", "Report.md", "--page", "letter", "--margin-mm", "15"]);
|
|
assert_eq!(letter.code, 0, "{}", letter.stderr);
|
|
let bad_page = run(&vault, &["export", "pdf", "Report.md", "--page", "a3"]);
|
|
assert_eq!(bad_page.code, 1, "an unknown page size is a command error");
|
|
let bad_margin = run(&vault, &["export", "pdf", "Report.md", "--margin-mm", "0"]);
|
|
assert_eq!(bad_margin.code, 2, "a non-positive margin is a usage error");
|
|
let bad_number = run(&vault, &["export", "pdf", "Report.md", "--margin-mm", "wide"]);
|
|
assert_eq!(bad_number.code, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn print_is_accepted_and_reports_print_unavailable() {
|
|
let vault = fixture("export-print");
|
|
let json = run(&vault, &["--json", "export", "pdf", "Report.md", "--print"]);
|
|
assert_eq!(json.code, 0, "{}", json.stderr);
|
|
assert_eq!(json.result().get("print").and_then(Value::as_str), Some("print_unavailable"));
|
|
let text = run(&vault, &["export", "pdf", "Report.md", "--print"]);
|
|
assert!(text.stdout.contains("print: print_unavailable"), "{}", text.stdout);
|
|
}
|
|
|
|
#[test]
|
|
fn a_bad_export_line_is_a_usage_error() {
|
|
let vault = fixture("export-usage");
|
|
assert_eq!(run(&vault, &["export", "docx", "Report.md"]).code, 2, "only pdf exists");
|
|
assert_eq!(run(&vault, &["export", "pdf"]).code, 2, "the path is required");
|
|
assert_eq!(run(&vault, &["export", "pdf", "Report.md", "--nope"]).code, 2);
|
|
assert_eq!(run(&vault, &["export", "pdf", "Missing.md"]).code, 1, "a missing note is a command error");
|
|
assert!(run(&vault, &["help"]).stdout.contains("export pdf <path>"), "the usage text lists the subcommand");
|
|
}
|
|
|
|
/// An absolute whiteboard `--out` is written by the CLI itself and never overwrites an existing file.
|
|
#[test]
|
|
fn whiteboard_svg_to_a_host_path_refuses_to_overwrite() {
|
|
let vault = empty_vault("export-wb-host");
|
|
note(&vault, "Board.draw", r#"{"type":"excalidraw","version":2,"elements":[],"appState":{},"files":{}}"#);
|
|
let target = vault.join("host-out.svg");
|
|
let out = target.display().to_string();
|
|
let first = run(&vault, &["--json", "export", "whiteboard", "Board.draw", "--out", out.as_str()]);
|
|
assert_eq!(first.code, 0, "{}", first.stderr);
|
|
assert!(target.exists(), "an absolute --out is written as asked");
|
|
let second = run(&vault, &["--json", "export", "whiteboard", "Board.draw", "--out", out.as_str()]);
|
|
assert_ne!(second.code, 0, "the second export must not overwrite");
|
|
}
|