mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 18:14:59 +00:00
M12: preSign runs as signing begins, so the signer sees what it changes; taken back if signing is cancelled; opt-in through sign_document
This commit is contained in:
@@ -248,11 +248,33 @@ impl Automation {
|
||||
Some(v) => labels_arg(v)?,
|
||||
};
|
||||
let out = self.resolve(a.str("out")?, true)?;
|
||||
// A dynamic form's preSign scripts run first, so what they change is signed with it.
|
||||
// A dynamic form's signing scripts run only when asked: what preSign changes is signed,
|
||||
// and the caller hasn't seen it. What they changed is listed.
|
||||
let run_scripts = a.opt_bool("run_form_scripts")?.unwrap_or(false);
|
||||
let signing = field.clone();
|
||||
let scripted = signing.as_deref().is_some_and(|f| {
|
||||
self.session.sign_event_scripted(doc_id, f, SignEvent::Before) || self.session.sign_event_scripted(doc_id, f, SignEvent::After)
|
||||
});
|
||||
// Each field's value and whether it is read-only, to tell what the preSign scripts changed.
|
||||
let values = |me: &Automation| -> std::collections::BTreeMap<String, (Vec<String>, bool)> {
|
||||
me.session.get(doc_id).map(|d| d.form.iter().map(|f| (f.name.clone(), (f.value.clone(), f.read_only()))).collect()).unwrap_or_default()
|
||||
};
|
||||
let was = values(self);
|
||||
let before = match &signing {
|
||||
Some(f) => self.session.run_sign_event(doc_id, f, SignEvent::Before).map_err(failed)?,
|
||||
None => false,
|
||||
Some(f) if run_scripts => self.session.run_sign_event(doc_id, f, SignEvent::Before).map_err(failed)?,
|
||||
_ => false,
|
||||
};
|
||||
let changed: Vec<Value> = if before {
|
||||
values(self)
|
||||
.into_iter()
|
||||
.filter(|(name, now)| was.get(name) != Some(now))
|
||||
.map(|(name, (value, read_only))| {
|
||||
let (old, old_ro) = was.get(&name).cloned().unwrap_or_default();
|
||||
json!({ "field": name, "before": old, "after": value, "read_only_before": old_ro, "read_only": read_only })
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let opts = SignOptions {
|
||||
field,
|
||||
@@ -288,8 +310,8 @@ impl Automation {
|
||||
self.session.mark_signed(doc_id, signed.clone(), Some(path.clone())).map_err(failed)?;
|
||||
// Then its postSign scripts; what they change is a change after signing, not saved yet.
|
||||
let after = match &signing {
|
||||
Some(f) => self.session.run_sign_event(doc_id, f, SignEvent::After).map_err(failed)?,
|
||||
None => false,
|
||||
Some(f) if run_scripts => self.session.run_sign_event(doc_id, f, SignEvent::After).map_err(failed)?,
|
||||
_ => false,
|
||||
};
|
||||
let scripts = self.session.take_js_output(doc_id);
|
||||
let doc = self.doc(a)?;
|
||||
@@ -299,10 +321,13 @@ impl Automation {
|
||||
result["form_scripts"] = json!({
|
||||
"before_signing": before,
|
||||
"after_signing": after,
|
||||
"changed": changed,
|
||||
"alerts": scripts.alerts,
|
||||
"errors": scripts.errors,
|
||||
"unsaved_changes": self.doc(a)?.dirty,
|
||||
});
|
||||
} else if scripted {
|
||||
result["form_scripts"] = json!({ "skipped": true, "why": "the field has signing scripts; pass run_form_scripts: true to run them" });
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -774,7 +774,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
.ro()
|
||||
.cmd("sign.digital")
|
||||
.with(schema(json!({}), &[])),
|
||||
t("sign_document", "Sign a document", "Sign with a digital ID (a .p12/.pfx path, or on macOS a Keychain identity: \"keychain:<common name or fingerprint>\" from sign_keychain_ids, or on Windows a store identity: \"windows:<common name or fingerprint>\" from sign_windows_ids) and save the signed file to `out` (signing always saves, as in Acrobat; the document then shows the signed file). Sign an existing empty signature field (`field`), or a new one on `page` at `rect` (omit rect for an invisible signature). certify: no_changes, form_fill or comments makes a certification signature. graphic shows a picture where the signer's name would be large (Acrobat: Configure Signature Appearance): {\"image\": path} (PNG or JPEG), {\"text\": name} (typed in the Fill & Sign script font) or {\"strokes\": [[[x, y], …], …]} (y up, the drawing pad 1 wide); it needs a visible signature. appearance chooses what a visible signature shows (default: name, date and labels; reason, location and distinguished_name are off unless turned on). labels gives the words of its labels, e.g. in another language (default English: \"Digitally signed by\", \"DN: \", \"Reason: \", \"Location: \", \"Date: \"; include the separator); each must be in Western European characters (Windows-1252), the only ones the signature's font draws, or the call is refused naming the label and character. PAdES B-B, SHA-256 (SHA-384 for P-384 keys).")
|
||||
t("sign_document", "Sign a document", "Sign with a digital ID (a .p12/.pfx path, or on macOS a Keychain identity: \"keychain:<common name or fingerprint>\" from sign_keychain_ids, or on Windows a store identity: \"windows:<common name or fingerprint>\" from sign_windows_ids) and save the signed file to `out` (signing always saves, as in Acrobat; the document then shows the signed file). Sign an existing empty signature field (`field`), or a new one on `page` at `rect` (omit rect for an invisible signature). certify: no_changes, form_fill or comments makes a certification signature. graphic shows a picture where the signer's name would be large (Acrobat: Configure Signature Appearance): {\"image\": path} (PNG or JPEG), {\"text\": name} (typed in the Fill & Sign script font) or {\"strokes\": [[[x, y], …], …]} (y up, the drawing pad 1 wide); it needs a visible signature. appearance chooses what a visible signature shows (default: name, date and labels; reason, location and distinguished_name are off unless turned on). labels gives the words of its labels, e.g. in another language (default English: \"Digitally signed by\", \"DN: \", \"Reason: \", \"Location: \", \"Date: \"; include the separator); each must be in Western European characters (Windows-1252), the only ones the signature's font draws, or the call is refused naming the label and character. PAdES B-B, SHA-256 (SHA-384 for P-384 keys). On a dynamic (XFA) form, run_form_scripts: true runs the field's own signing scripts: preSign just before signing, so what it changes is signed too (form_scripts.changed lists each field it changed, with the values before and after), and postSign after, left unsaved. Without it they don't run, and form_scripts.skipped says the field has them.")
|
||||
.cmd("sign.digital")
|
||||
.with(schema(
|
||||
json!({
|
||||
@@ -825,6 +825,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
"additionalProperties": false,
|
||||
},
|
||||
"out": { "type": "string", "description": "Where to save the signed document." },
|
||||
"run_form_scripts": { "type": "boolean", "description": "Run a dynamic form's preSign and postSign scripts for the field (default false)." },
|
||||
}),
|
||||
&["doc", "id", "out"],
|
||||
)),
|
||||
|
||||
@@ -5767,9 +5767,10 @@ fn signing_with_an_image_typed_or_drawn_graphic_through_tools() {
|
||||
assert!(inked(60, 110), "the strokes are drawn");
|
||||
}
|
||||
|
||||
/// Signing a dynamic form's signature box through sign_document runs its XFA preSign scripts first,
|
||||
/// so what they change is signed, and its postSign scripts after, left as unsaved changes; the
|
||||
/// result says so and carries their messages. A box without such scripts reports none.
|
||||
/// Signing a dynamic form's signature box through sign_document runs its XFA signing scripts only
|
||||
/// when asked (run_form_scripts): preSign first, so what it changes is signed, listed field by field,
|
||||
/// and postSign after, left as unsaved changes; the result carries their messages. Not asked, they
|
||||
/// don't run and the result says the box has them; a box without them reports nothing.
|
||||
#[test]
|
||||
fn signing_an_xfa_box_runs_its_signing_scripts_through_tools() {
|
||||
let dir = workdir("xfa-sign-events");
|
||||
@@ -5788,8 +5789,29 @@ fn signing_an_xfa_box_runs_its_signing_scripts_through_tools() {
|
||||
.unwrap()
|
||||
.to_string()
|
||||
};
|
||||
let r = ok(&mut a, "sign_document", json!({ "doc": doc, "id": "ada.p12", "password": "secret12", "field": name("sign"), "out": "signed.pdf" }));
|
||||
// Not asked: the scripts don't run, and the result says the box has them.
|
||||
let r = ok(&mut a, "sign_document", json!({ "doc": doc, "id": "ada.p12", "password": "secret12", "field": name("sign"), "out": "unasked.pdf" }));
|
||||
assert_eq!(r["form_scripts"]["skipped"], json!(true), "{r}");
|
||||
let unasked = ok(&mut a, "doc_open", json!({ "path": "unasked.pdf" }))["doc"].as_u64().unwrap();
|
||||
let unasked = ok(&mut a, "form_fields", json!({ "doc": unasked }));
|
||||
assert!(
|
||||
unasked["fields"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|f| f["name"].as_str().is_some_and(|n| n == "when" || n.ends_with(".when[0]")) && f["value"].is_null()),
|
||||
"{unasked}"
|
||||
);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "form.pdf" }))["doc"].as_u64().unwrap();
|
||||
let r = ok(
|
||||
&mut a,
|
||||
"sign_document",
|
||||
json!({ "doc": doc, "id": "ada.p12", "password": "secret12", "field": name("sign"), "out": "signed.pdf", "run_form_scripts": true }),
|
||||
);
|
||||
let scripts = &r["form_scripts"];
|
||||
let changed = scripts["changed"].as_array().unwrap();
|
||||
assert_eq!(changed.len(), 1, "only what preSign changed: {r}");
|
||||
assert_eq!((&changed[0]["after"], &changed[0]["read_only"]), (&json!(["signed"]), &json!(true)), "{r}");
|
||||
assert_eq!((scripts["before_signing"].as_bool(), scripts["after_signing"].as_bool()), (Some(true), Some(true)), "{r}");
|
||||
assert_eq!(scripts["alerts"], json!(["Signing notice"]), "{r}");
|
||||
assert_eq!(scripts["unsaved_changes"], json!(true), "the postSign change isn't in the signed file: {r}");
|
||||
@@ -5809,7 +5831,11 @@ fn signing_an_xfa_box_runs_its_signing_scripts_through_tools() {
|
||||
let list = ok(&mut a, "sign_list", json!({ "doc": signed }));
|
||||
assert!(list["signatures"].as_array().unwrap().iter().any(|s| s["signed"] == true && s["status"] == "unknown"), "intact: {list}");
|
||||
// A box without signing scripts: nothing reported.
|
||||
let r = ok(&mut a, "sign_document", json!({ "doc": signed, "id": "ada.p12", "password": "secret12", "field": name("plain"), "out": "both.pdf" }));
|
||||
let r = ok(
|
||||
&mut a,
|
||||
"sign_document",
|
||||
json!({ "doc": signed, "id": "ada.p12", "password": "secret12", "field": name("plain"), "out": "both.pdf", "run_form_scripts": true }),
|
||||
);
|
||||
assert!(r.get("form_scripts").is_none(), "{r}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
+20
-6
@@ -285,17 +285,31 @@ impl Session {
|
||||
/// there are none: not a laid-out XFA form, JavaScript off, no such field, or no such script.
|
||||
/// What the scripts said or asked for is in [`Session::take_js_output`].
|
||||
pub fn run_sign_event(&mut self, id: DocId, field: &str, event: SignEvent) -> Result<bool, EditError> {
|
||||
if self.js_off {
|
||||
return Ok(false);
|
||||
if self.get(id).is_none() {
|
||||
return Err(EditError::NoDocument);
|
||||
}
|
||||
let doc = self.get(id).ok_or(EditError::NoDocument)?;
|
||||
let (Some(tpl), Some(editor)) = (doc.xfa_template.clone(), doc.editor.as_ref()) else { return Ok(false) };
|
||||
let Some(f) = doc.form.iter().find(|f| f.name == field) else { return Ok(false) };
|
||||
let Some(som) = crate::xfa::scripted_som(&editor.cos, &tpl, f, event.activity()) else { return Ok(false) };
|
||||
let Some(som) = self.sign_event_som(id, field, event) else { return Ok(false) };
|
||||
self.apply(id, Edit::XfaEvent { som, activity: event.activity().into() })?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Whether signature field `field` of document `id` has XFA signing scripts for `event` that
|
||||
/// [`Session::run_sign_event`] would run, without running them.
|
||||
pub fn sign_event_scripted(&self, id: DocId, field: &str, event: SignEvent) -> bool {
|
||||
self.sign_event_som(id, field, event).is_some()
|
||||
}
|
||||
|
||||
/// The SOM path of signature field `field` when it has scripts for `event` to run.
|
||||
fn sign_event_som(&self, id: DocId, field: &str, event: SignEvent) -> Option<String> {
|
||||
if self.js_off {
|
||||
return None;
|
||||
}
|
||||
let doc = self.get(id)?;
|
||||
let (tpl, editor) = (doc.xfa_template.as_ref()?, doc.editor.as_ref()?);
|
||||
let f = doc.form.iter().find(|f| f.name == field)?;
|
||||
crate::xfa::scripted_som(&editor.cos, tpl, f, event.activity())
|
||||
}
|
||||
|
||||
/// Run `script` in document `id`, for a button (`target`: the field, event "Mouse Up") or
|
||||
/// the console (no target). Field changes and `resetForm` are applied as one undoable step;
|
||||
/// everything else the script asked for is returned.
|
||||
|
||||
@@ -695,6 +695,11 @@ impl Document {
|
||||
self.redo_label().map(Label::as_str)
|
||||
}
|
||||
|
||||
/// How many steps Undo can take back: a step still at this depth is still the last one.
|
||||
pub fn undo_depth(&self) -> usize {
|
||||
self.editor.as_ref().map_or(0, |e| e.undo.len())
|
||||
}
|
||||
|
||||
/// What Undo would undo, in parts, for an interface that says it in another language.
|
||||
pub fn undo_label(&self) -> Option<&Label> {
|
||||
self.editor.as_ref().and_then(|e| e.undo.last()).map(|(l, ..)| l)
|
||||
|
||||
@@ -812,6 +812,9 @@ pub struct PdfCraftApp {
|
||||
pub summary_sort: pdfcraft_engine::SummarySort,
|
||||
/// The signing dialogs' state.
|
||||
pub sign_draft: Option<SignDraft>,
|
||||
/// A dynamic form's preSign step, run when signing a field began: the document and its undo
|
||||
/// depth after the step. Taken back if signing ends without a signature.
|
||||
pub(crate) presign: Option<(DocId, usize)>,
|
||||
/// Digital ID files the user has created or added.
|
||||
pub digital_ids: Vec<DigitalIdEntry>,
|
||||
/// Signatures panel: expanded entries (field names).
|
||||
@@ -1185,6 +1188,7 @@ impl PdfCraftApp {
|
||||
rotate_draft: RotateDraft::default(),
|
||||
summary_sort: Default::default(),
|
||||
sign_draft: None,
|
||||
presign: None,
|
||||
digital_ids: Vec::new(),
|
||||
sig_expanded: Vec::new(),
|
||||
a11y_options: a11y_ui::A11yOptions::default(),
|
||||
@@ -3590,6 +3594,7 @@ impl eframe::App for PdfCraftApp {
|
||||
self.process_file_requests();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.process_picked();
|
||||
self.settle_presign();
|
||||
// Last, after everything that can open or answer the "save changes?" question.
|
||||
self.attend_save_prompt(ctx);
|
||||
}
|
||||
|
||||
@@ -272,10 +272,11 @@ impl PdfCraftApp {
|
||||
self.write_files(&[(name, std::sync::Arc::new(certificate_pem(c).into_bytes()))], "Export certificate");
|
||||
}
|
||||
|
||||
/// Start signing: the rectangle (or field) is known; show Sign with a Digital ID.
|
||||
/// Start signing: the rectangle (or field) is known; show Sign with a Digital ID, after a
|
||||
/// dynamic form's preSign scripts for the field.
|
||||
pub fn start_signing(&mut self, page: usize, rect: Option<[f64; 4]>, field: Option<String>, certify: Option<u8>) {
|
||||
self.refresh_os_key_store_ids();
|
||||
let mut draft = SignDraft::new(page, rect, field, certify, &self.digital_ids);
|
||||
let mut draft = SignDraft::new(page, rect, field.clone(), certify, &self.digital_ids);
|
||||
draft.graphic = match &self.sign_graphic {
|
||||
SignGraphic::FillSign if self.signature.is_none() => SignGraphic::Name,
|
||||
g => g.clone(),
|
||||
@@ -285,6 +286,55 @@ impl PdfCraftApp {
|
||||
}
|
||||
self.sign_draft = Some(draft);
|
||||
self.dialog = Some(crate::Dialog::Sign);
|
||||
// A dynamic form's preSign scripts run now, as signing the field begins, so what they
|
||||
// change and say shows before the signer confirms (DA 31's date the signature and lock the
|
||||
// date). Signing ended without a signature takes them back ([`Self::settle_presign`]).
|
||||
if let Some(f) = &field
|
||||
&& let Some((_, doc)) = self.active_ids()
|
||||
{
|
||||
self.run_presign(doc, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run signature field `field`'s preSign scripts in document `doc` and keep the step pending.
|
||||
fn run_presign(&mut self, doc: pdfcraft_engine::DocId, field: &str) {
|
||||
match self.session.run_sign_event(doc, field, pdfcraft_engine::js::SignEvent::Before) {
|
||||
Ok(true) => {
|
||||
self.presign = self.session.get(doc).map(|d| (doc, d.undo_depth()));
|
||||
self.refresh_view_of(doc);
|
||||
let out = self.session.take_js_output(doc);
|
||||
self.handle_js(doc, out);
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take back a pending preSign step once signing has ended without a signature: the Sign
|
||||
/// dialog closed and no save picker is open for it. Only a step still the last one is taken
|
||||
/// back. Called every frame.
|
||||
pub(crate) fn settle_presign(&mut self) {
|
||||
let Some((doc, depth)) = self.presign else { return };
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.pickers.pending() {
|
||||
return;
|
||||
}
|
||||
if self.dialog == Some(crate::Dialog::Sign) {
|
||||
return;
|
||||
}
|
||||
self.presign = None;
|
||||
if self.session.get(doc).is_some_and(|d| d.undo_depth() == depth) && self.session.undo(doc).is_ok() {
|
||||
self.refresh_view_of(doc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show document `doc`'s changed form in its view.
|
||||
fn refresh_view_of(&mut self, doc: pdfcraft_engine::DocId) {
|
||||
if let Some(i) = self.views.iter().position(|v| v.id == doc)
|
||||
&& let Some(info) = self.session.get(doc).map(|d| d.info.clone())
|
||||
{
|
||||
self.views[i].document_changed(&info);
|
||||
}
|
||||
}
|
||||
|
||||
/// List OS key store signing identities (after the file-based IDs).
|
||||
@@ -459,23 +509,14 @@ impl PdfCraftApp {
|
||||
if !app.commit_typing_in(doc_id) {
|
||||
return Err(String::new());
|
||||
}
|
||||
// A dynamic form's preSign scripts run first, so what they change is signed with it
|
||||
// (DA 31's date the signature and lock the date). What they say shows once signing is
|
||||
// done, so the notice that it was saved doesn't take the place of the form's own.
|
||||
// The form's preSign step (run when signing began) is signed with it; if signing fails
|
||||
// the step stays pending, and is taken back if signing ends without a signature.
|
||||
let field = opts.field.clone();
|
||||
let before = match &field {
|
||||
Some(f) => app.session.run_sign_event(doc_id, f, pdfcraft_engine::js::SignEvent::Before).map_err(|e| e.to_string())?,
|
||||
None => false,
|
||||
};
|
||||
let mut said = app.session.take_js_output(doc_id);
|
||||
let pending = app.presign.take();
|
||||
let signed = match app.session.sign(doc_id, &id, opts) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
// Not signed: what the preSign scripts did is taken back.
|
||||
if before {
|
||||
let _ = app.session.undo(doc_id);
|
||||
}
|
||||
app.js_console.log.extend(said.console);
|
||||
app.presign = pending;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
};
|
||||
@@ -485,15 +526,11 @@ impl PdfCraftApp {
|
||||
app.watch.known_now(doc_id, &path.to_string_lossy());
|
||||
app.session.mark_signed(doc_id, signed, Some(path.to_string_lossy().into_owned())).map_err(|e| e.to_string())?;
|
||||
// Then its postSign scripts; what they change is a change after signing, to save.
|
||||
// What they say shows once signing is done, after the notice that it was saved.
|
||||
if let Some(f) = &field {
|
||||
app.session.run_sign_event(doc_id, f, pdfcraft_engine::js::SignEvent::After).map_err(|e| e.to_string())?;
|
||||
let after = app.session.take_js_output(doc_id);
|
||||
said.alerts.extend(after.alerts);
|
||||
said.console.extend(after.console);
|
||||
said.requests.extend(after.requests);
|
||||
said.errors.extend(after.errors);
|
||||
said.doc_errors.extend(after.doc_errors);
|
||||
}
|
||||
let said = app.session.take_js_output(doc_id);
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::macos_association::repair(&path);
|
||||
app.sign_graphic = choice;
|
||||
|
||||
@@ -368,9 +368,9 @@ fn clicking_an_empty_signature_field_signs_it() {
|
||||
assert!(sig.signed && sig.visible);
|
||||
}
|
||||
|
||||
/// Signing a dynamic form's signature box in the app runs its XFA preSign script first, so the
|
||||
/// value it sets is in the signed file and its notice shows, then its postSign script, whose
|
||||
/// change is left to save.
|
||||
/// Signing a dynamic form's signature box in the app runs its XFA preSign script as signing begins,
|
||||
/// so its notice and the value it sets show before the signer confirms, and that value is in the
|
||||
/// signed file; its postSign script runs once signed, its change left to save.
|
||||
#[test]
|
||||
fn signing_an_xfa_box_runs_its_signing_scripts_in_the_app() {
|
||||
let dir = dir().join("xfa-events");
|
||||
@@ -396,6 +396,8 @@ fn signing_an_xfa_box_runs_its_signing_scripts_in_the_app() {
|
||||
h.drop_at(p);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Sign));
|
||||
assert!(h.query_by_label_contains("Signing notice").is_some(), "the preSign script's notice shows before signing");
|
||||
assert_eq!(named(&h, "when").value, ["signed"], "and what it set, under the dialog");
|
||||
let entry = {
|
||||
let id = pdfcraft_engine::sign::pkcs12::open(
|
||||
&std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12")).unwrap(),
|
||||
@@ -413,7 +415,6 @@ fn signing_an_xfa_box_runs_its_signing_scripts_in_the_app() {
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Sign").click();
|
||||
h.run_steps(6);
|
||||
assert!(h.query_by_label_contains("Signing notice").is_some(), "the preSign script's notice shows");
|
||||
// The signed file holds the preSign script's value; the postSign one waits to be saved.
|
||||
let signed = std::fs::read(dir.join("contract_signed.pdf")).unwrap();
|
||||
let mut s = pdfcraft_engine::Session::new();
|
||||
@@ -427,6 +428,31 @@ fn signing_an_xfa_box_runs_its_signing_scripts_in_the_app() {
|
||||
assert!(st.session.get(st.views[1].id).unwrap().dirty, "the postSign change is not saved yet");
|
||||
}
|
||||
|
||||
/// Signing a dynamic form's box and then cancelling takes back what its preSign script changed.
|
||||
#[test]
|
||||
fn cancelling_signing_takes_back_the_presign_scripts_changes() {
|
||||
let dir = dir().join("xfa-events-cancel");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut h = harness(dir.clone());
|
||||
let bytes = pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::signing_events_template());
|
||||
h.state_mut().open_bytes("events.pdf", None, bytes).unwrap();
|
||||
h.state_mut().active = Some(1);
|
||||
crate::support::settle(&mut h);
|
||||
let named = |h: &Harness<'static, PdfCraftApp>, n: &str| {
|
||||
let s = h.state();
|
||||
s.session.get(s.views[1].id).unwrap().form.iter().find(|f| f.name == n || f.name.ends_with(&format!(".{n}[0]"))).cloned().unwrap()
|
||||
};
|
||||
let sign = named(&h, "sign").name;
|
||||
h.state_mut().start_signing(0, None, Some(sign), None);
|
||||
h.run_steps(2);
|
||||
assert_eq!(named(&h, "when").value, ["signed"]);
|
||||
h.state_mut().dialog = None;
|
||||
h.state_mut().sign_draft = None;
|
||||
h.run_steps(3);
|
||||
assert!(named(&h, "when").value.is_empty(), "taken back");
|
||||
assert!(!named(&h, "when").read_only(), "and unlocked again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fill_and_sign_signature_or_an_image_shows_in_a_digital_signature() {
|
||||
use pdfcraft_ui_egui::SignGraphic;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user