M12: a batched focus event that changes nothing keeps the batch's edits, and an exit is noted as run only when it ran

This commit is contained in:
Eric Kaden committed 2026-10-23 23:52:53 -04:00
1 parent 2e8720c332
commit 3257bac8e0
3 files changed
+59 -7

No files matched your search

+5 -2
View File
@@ -3866,8 +3866,9 @@ impl Session {
guard(|| run_edit(&mut next, &edit, &mut cx))
.unwrap_or_else(|m| Err(EditError::Invalid(format!("{} failed unexpectedly ({m}); the document was not changed", edit.label()))))?;
// A field's enter or exit scripts that changed nothing leave no edit behind (no undo step,
// nothing unsaved); what they showed or asked for still goes out.
if cx.xfa_unchanged {
// nothing unsaved); what they showed or asked for still goes out. Only an event on its own:
// a batch keeps the other edits it holds.
if cx.xfa_unchanged && matches!(edit, Edit::XfaEvent { .. }) {
doc.js_output.append(cx.xfa_out);
if cx.xfa_ran_away {
doc.xfa_template = None;
@@ -4037,6 +4038,7 @@ impl Session {
Ok(())
}
/// Undo the last edit of document `id`, keeping it for [`Session::redo`]. Returns its label.
pub fn undo(&mut self, id: DocId) -> Result<Label, EditError> {
let doc = self.doc_mut(id)?;
let editor = doc.editor.as_mut().ok_or(EditError::NothingToUndo)?;
@@ -4068,6 +4070,7 @@ impl Session {
Ok(label)
}
/// Redo the last edit of document `id` that [`Session::undo`] took back. Returns its label.
pub fn redo(&mut self, id: DocId) -> Result<Label, EditError> {
let doc = self.doc_mut(id)?;
let editor = doc.editor.as_mut().ok_or(EditError::NothingToRedo)?;
+44
View File
@@ -4741,6 +4741,50 @@ fn an_exit_skip_does_not_outlive_its_change() {
assert_eq!(field_text(&s, id, "exits"), (before.parse::<u32>().unwrap() + 1).to_string(), "after another edit, the exit runs");
}
/// A focus event that changes nothing, batched with a form edit, doesn't cost the batch its edit.
#[test]
fn a_batch_keeps_its_edits_when_its_focus_event_changes_nothing() {
let mut s = Session::new();
let id = s.open("form.pdf", None, focus_form(), None).expect("opens");
let som = {
let doc = s.get(id).unwrap();
let quiet = doc.form.iter().find(|f| f.name == "quiet").unwrap();
crate::xfa::widget_som(&doc.editor.as_ref().unwrap().cos, quiet, 0).unwrap()
};
let batch = Edit::Batch {
label: "Fill and leave".into(),
edits: vec![
Edit::SetFieldValue { name: "quiet".into(), value: FieldValue::Text("z".into()) },
Edit::XfaEvent { som, activity: "exit".into() },
],
};
s.apply(id, batch).unwrap();
assert_eq!(field_text(&s, id, "quiet"), "z", "the value is kept");
assert!(s.get(id).unwrap().dirty);
}
/// A field's exit is noted as run (for the focus loss to skip) only when an exit script ran:
/// with every script skipped, as when the event's budget is spent or a script ran away, nothing
/// is noted and the next focus loss runs the exit.
#[test]
fn on_changes_notes_only_the_exits_that_ran() {
let mut s = Session::new();
let id = s.open("form.pdf", None, focus_form(), None).expect("opens");
let doc = s.get(id).unwrap();
let mut cos = doc.editor.as_ref().unwrap().cos.clone();
let tpl = doc.xfa_template.clone().unwrap();
let names = vec!["probe".to_string()];
let mut exited = Vec::new();
let mut out = crate::js::JsOutput::default();
let mut stopped = crate::xfa::XfaRun { page_count: 1, out: &mut out, datasets: None, ran_away: true };
crate::xfa::on_changes(&mut cos, &tpl, &names, &mut stopped, &mut exited).unwrap();
assert!(exited.is_empty(), "no exit ran: {exited:?}");
let mut out = crate::js::JsOutput::default();
let mut running = crate::xfa::XfaRun { page_count: 1, out: &mut out, datasets: None, ran_away: false };
crate::xfa::on_changes(&mut cos, &tpl, &names, &mut running, &mut exited).unwrap();
assert_eq!(exited.len(), 1, "the exit ran: {exited:?}");
}
/// Scripts that change nothing leave no edit: the document stays saved, with nothing to undo,
/// and what they showed still reaches the viewer. A field without such scripts runs nothing.
#[test]
+10 -5
View File
@@ -839,13 +839,18 @@ pub(crate) fn on_changes(
let som = r.by_som().iter().find(|(_, f)| *f == name).map(|(s, _)| s.clone());
let Some(som) = som else { continue };
let value = r.fields().get(name).map(|f| f.value.join("\n")).unwrap_or_default();
let exits = r.events_of(&som, "exit");
if !exits.is_empty() {
exited.push(som.clone());
}
for ev in r.events_of(&som, "change").into_iter().chain(exits) {
for ev in r.events_of(&som, "change") {
r.run(&ev, &value)?;
}
// Noted only when an exit script ran: one the event's budget skipped, or one an error
// before it stopped, is still to run when the field loses focus.
let mut ran_exit = false;
for ev in r.events_of(&som, "exit") {
ran_exit |= r.run(&ev, &value)?.is_some();
}
if ran_exit {
exited.push(som.clone());
}
for ev in r.events_of(&som, "validate") {
let Some(o) = r.run(&ev, &value)? else { continue };
if (o.result_bool == Some(false) || o.error.is_some()) && !value.trim().is_empty() {