fix(automation): XFA clicks and form edits return only the script output they produced (#4559)

This commit is contained in:
.CooCoo authored and GitHub committed 2026-11-09 18:13:50 +08:00
1 parent ab502255cb
commit 17425b0147
8 files changed
+276 -37

No files matched your search

+40 -15
View File
@@ -213,25 +213,25 @@ const MAX_TAG_PAGE: usize = 1000;
impl Automation {
pub(crate) fn accessibility_autotag_apply(&mut self, a: &Args) -> Result<Value> {
let id = self.doc(a)?.id;
self.session.apply(id, pdfcraft_engine::Edit::AutoTagText).map_err(failed)?;
let warnings = self.apply_with_warnings(id, pdfcraft_engine::Edit::AutoTagText)?;
let tags = self.accessibility_tags(a)?;
Ok(json!({
let out = json!({
"tags": tags["tags"], "count": tags["count"],
"note": "Text objects were tagged as paragraphs. Review heading roles, reading order and remaining accessibility findings manually.",
}))
});
Ok(with_warnings(out, warnings))
}
pub(crate) fn accessibility_autotag_form_fields(&mut self, a: &Args) -> Result<Value> {
let id = self.doc(a)?.id;
self.session.apply(id, pdfcraft_engine::Edit::AutoTagFormFields).map_err(failed)?;
let warnings = self.apply_with_warnings(id, pdfcraft_engine::Edit::AutoTagFormFields)?;
let report = self
.doc(a)?
.accessibility_check(&Options { rules: [Rule::TaggedFormFields].into(), pages: None })
.ok_or_else(|| failed("the document can't be read"))?;
let result = report.result(Rule::TaggedFormFields).ok_or_else(|| failed("the form field check is unavailable"))?;
Ok(
json!({ "status": status_id(result.status), "remaining_findings": result.findings.len(), "note": "Only form widgets were tagged. Review text, figures, links, reading order, and other document content separately." }),
)
let out = json!({ "status": status_id(result.status), "remaining_findings": result.findings.len(), "note": "Only form widgets were tagged. Review text, figures, links, reading order, and other document content separately." });
Ok(with_warnings(out, warnings))
}
pub(crate) fn accessibility_autotag_preview(&self, a: &Args) -> Result<Value> {
@@ -432,10 +432,11 @@ impl Automation {
let rule = Rule::from_id(id).ok_or_else(|| ToolError::InvalidArgs(format!("unknown rule {id:?}")))?;
let edit = self.doc(a)?.accessibility_fix(rule, a.opt_str("value")?).map_err(failed)?;
let doc = self.doc(a)?.id;
self.session.apply(doc, edit).map_err(failed)?;
let warnings = self.apply_with_warnings(doc, edit)?;
let r =
self.doc(a)?.accessibility_check(&Options { rules: [rule].into(), pages: None }).ok_or_else(|| failed("the document can't be read"))?;
Ok(json!({ "rule": id, "status": status_id(r.results.iter().find(|x| x.rule == rule).map_or(Status::Skipped, |x| x.status)) }))
let out = json!({ "rule": id, "status": status_id(r.results.iter().find(|x| x.rule == rule).map_or(Status::Skipped, |x| x.status)) });
Ok(with_warnings(out, warnings))
}
pub(crate) fn accessibility_figures(&self, a: &Args) -> Result<Value> {
@@ -466,8 +467,8 @@ impl Automation {
pdfcraft_engine::Edit::SetAltText { figure, alt: a.opt_str("alt")?.map(str::to_owned) }
};
let id = self.doc(a)?.id;
self.session.apply(id, edit).map_err(failed)?;
self.accessibility_figures(a)
let warnings = self.apply_with_warnings(id, edit)?;
Ok(with_warnings(self.accessibility_figures(a)?, warnings))
}
/// The structure tags, one page at a time (`offset`, `limit`), optionally only some types, pages
@@ -551,8 +552,9 @@ impl Automation {
let generation = u16::try_from(a.int("generation")?)
.map_err(|_| ToolError::InvalidArgs("generation must be a PDF object generation from accessibility_tags".into()))?;
let id = self.doc(a)?.id;
self.session.apply(id, pdfcraft_engine::Edit::MarkTagArtifact { object: pdfcraft_cos::ObjRef::new(object, generation) }).map_err(failed)?;
self.accessibility_tags(a)
let warnings =
self.apply_with_warnings(id, pdfcraft_engine::Edit::MarkTagArtifact { object: pdfcraft_cos::ObjRef::new(object, generation) })?;
Ok(with_warnings(self.accessibility_tags(a)?, warnings))
}
pub(crate) fn accessibility_move_tag(&mut self, a: &Args) -> Result<Value> {
@@ -573,9 +575,32 @@ impl Automation {
other => return Err(ToolError::InvalidArgs(format!("placement must be before or after, not {other:?}"))),
};
let id = self.doc(a)?.id;
self.session.apply(id, pdfcraft_engine::Edit::MoveTag { object, anchor, placement }).map_err(failed)?;
self.accessibility_tags(a)
let warnings = self.apply_with_warnings(id, pdfcraft_engine::Edit::MoveTag { object, anchor, placement })?;
Ok(with_warnings(self.accessibility_tags(a)?, warnings))
}
/// Apply `edit` to document `id` and take out the errors it added to the document's script
/// output, such as the values of an XFA form that could not be written to its data.
fn apply_with_warnings(&mut self, id: DocId, edit: pdfcraft_engine::Edit) -> Result<Vec<String>> {
let ((), out) = self.session.with_own_js_output(id, |s| s.apply(id, edit)).map_err(failed)?;
Ok(out.errors)
}
}
/// `out` with `warnings` added to its `warnings` list (made when it has none).
fn with_warnings(mut out: Value, warnings: Vec<String>) -> Value {
if warnings.is_empty() {
return out;
}
if let Some(map) = out.as_object_mut() {
match map.get_mut("warnings").and_then(Value::as_array_mut) {
Some(list) => list.extend(warnings.into_iter().map(Value::from)),
None => {
map.insert("warnings".into(), json!(warnings));
}
}
}
out
}
#[cfg(test)]
+5 -5
View File
@@ -1,4 +1,4 @@
use super::{Args, Automation, Result, ToolError, Value, child, failed, json, write_atomic};
use super::{Args, Automation, Result, ToolError, Value, child, failed, json, with_warnings, write_atomic};
impl Automation {
pub(crate) fn doc_export_office(&self, a: &Args) -> Result<Value> {
@@ -103,8 +103,8 @@ impl Automation {
pub(crate) fn pdfa_convert(&mut self, a: &Args) -> Result<Value> {
let level = self.pdfa_level(a)?;
let id = self.doc(a)?.id;
self.session.apply(id, pdfcraft_engine::Edit::ConvertPdfA { level }).map_err(failed)?;
self.pdfa_verify(a)
let warnings = self.apply_with_warnings(id, pdfcraft_engine::Edit::ConvertPdfA { level })?;
Ok(with_warnings(self.pdfa_verify(a)?, warnings))
}
pub(crate) fn action_list(&self) -> Result<Value> {
@@ -209,7 +209,7 @@ impl Automation {
pub(crate) fn doc_compare_mark(&mut self, a: &Args) -> Result<Value> {
let (old, new) = self.compare_ids(a)?;
let n = self.session.mark_differences(old, new).map_err(failed)?;
Ok(json!({ "comments": n }))
let (n, out) = self.session.with_own_js_output(new, |s| s.mark_differences(old, new)).map_err(failed)?;
Ok(with_warnings(json!({ "comments": n }), out.errors))
}
}
+20 -11
View File
@@ -1,4 +1,4 @@
use super::{Args, Automation, Result, ToolError, Value, failed, json, write_atomic};
use super::{Args, Automation, Result, ToolError, Value, failed, json, with_warnings, write_atomic};
/// A script's request to the viewer as the JSON that running a script reports; a side panel
/// goes by its fixed id.
@@ -28,10 +28,18 @@ impl Automation {
/// messages, console lines, requests, error, the failing document scripts' errors and result.
pub(crate) fn js_run(&mut self, a: &Args) -> Result<Value> {
let id = self.doc(a)?.id;
let o = self.session.run_javascript(id, a.str("script")?, a.opt_str("field")?).map_err(failed)?;
let (script, field) = (a.str("script")?, a.opt_str("field")?);
// Applying the script's field changes runs the field scripts again; their output is reported with the script's.
let (mut o, edit) = self.session.with_own_js_output(id, |s| s.run_javascript(id, script, field)).map_err(failed)?;
o.alerts.extend(edit.alerts);
o.console.extend(edit.console);
o.requests.extend(edit.requests);
let reqs: Vec<Value> = o.requests.iter().map(request_json).collect();
let alerts: Vec<Value> = o.alerts.iter().map(alert_json).collect();
Ok(json!({ "alerts": alerts, "console": o.console, "requests": reqs, "error": o.error, "document_errors": o.doc_errors, "result": o.result }))
Ok(with_warnings(
json!({ "alerts": alerts, "console": o.console, "requests": reqs, "error": o.error, "document_errors": o.doc_errors, "result": o.result }),
edit.errors,
))
}
pub(crate) fn js_document_scripts(&self, a: &Args) -> Result<Value> {
@@ -42,8 +50,8 @@ impl Automation {
pub(crate) fn js_set_document_script(&mut self, a: &Args) -> Result<Value> {
let edit = pdfcraft_engine::Edit::SetDocumentScript { name: a.str("name")?.into(), script: a.opt_str("script")?.map(str::to_string) };
let id = self.doc(a)?.id;
self.session.apply(id, edit).map_err(failed)?;
self.js_document_scripts(a)
let warnings = self.apply_with_warnings(id, edit)?;
Ok(with_warnings(self.js_document_scripts(a)?, warnings))
}
pub(crate) fn form_set_script(&mut self, a: &Args) -> Result<Value> {
@@ -53,8 +61,7 @@ impl Automation {
script: a.opt_str("script")?.map(str::to_string),
};
let id = self.doc(a)?.id;
self.session.apply(id, edit).map_err(failed)?;
let out = self.session.take_js_output(id);
let ((), out) = self.session.with_own_js_output(id, |s| s.apply(id, edit)).map_err(failed)?;
Ok(json!({ "field": a.str("field")?, "event": a.str("event")?, "console": out.console, "errors": out.errors }))
}
@@ -90,10 +97,12 @@ impl Automation {
v
})
.collect();
let mut warnings = Vec::new();
if a.opt_bool("add")?.unwrap_or(true) && !found.is_empty() {
self.session.auto_detect_fields(id, &pages).map_err(failed)?;
let (_, out) = self.session.with_own_js_output(id, |s| s.auto_detect_fields(id, &pages)).map_err(failed)?;
warnings = out.errors;
}
Ok(json!({ "fields": list }))
Ok(with_warnings(json!({ "fields": list }), warnings))
}
pub(crate) fn form_actions(&self, a: &Args) -> Result<Value> {
@@ -157,8 +166,8 @@ impl Automation {
let edit =
pdfcraft_engine::Edit::SetFieldProps { name, props: Box::new(pdfcraft_engine::FieldProps { actions: Some(acts), ..Default::default() }) };
let id = self.doc(a)?.id;
self.session.apply(id, edit).map_err(failed)?;
self.form_actions(a)
let warnings = self.apply_with_warnings(id, edit)?;
Ok(with_warnings(self.form_actions(a)?, warnings))
}
pub(crate) fn js_enabled(&mut self, a: &Args) -> Result<Value> {
+4 -3
View File
@@ -1,4 +1,4 @@
use super::{Args, Automation, Result, ToolError, Value, child, failed, json, write_atomic};
use super::{Args, Automation, Result, ToolError, Value, child, failed, json, with_warnings, write_atomic};
impl Automation {
/// The settings every OCR tool shares: dpi, language, the engine/strategy choice, the four
@@ -258,9 +258,10 @@ impl Automation {
let recognizers = pdfcraft_engine::ocr::recognizers(&settings).map_err(failed)?;
match out {
None => {
let found = self.session.recognize_text(id, &pages, settings).map_err(failed)?;
let (found, out) = self.session.with_own_js_output(id, |s| s.recognize_text(id, &pages, settings)).map_err(failed)?;
let warnings = out.errors;
let words = found.iter().map(|p| p.words.len()).sum::<usize>();
Ok(json!({ "words": words, "pages": Self::ocr_pages_json(&found, copy) }))
Ok(with_warnings(json!({ "words": words, "pages": Self::ocr_pages_json(&found, copy) }), warnings))
}
Some(out) => {
let job = self.session.ocr_job(id, &pages, settings).ok_or_else(|| failed("the document could not be read"))?;
+1 -1
View File
@@ -1343,7 +1343,7 @@ pub fn tools() -> Vec<ToolDef> {
t("form_merge_data", "Merge data files into spreadsheet", "Collect the field values of form data files (FDF, XFDF) or filled-in PDF forms into one CSV file at path: a column per field name, a row per file. Returns the row and column counts.")
.cmd("form.merge_data")
.with(schema(json!({ "paths": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, "path": { "type": "string" } }), &["paths", "path"])),
t("js_run", "Run JavaScript", "Run Acrobat JavaScript in the document, as the JavaScript console does (or as push button `field`'s Mouse Up script when field is given; on a laid-out XFA form, `field` runs that button's XFA click script instead, JavaScript or FormCalc, which can add and remove rows and show or hide subforms). The form object model is available: this/getField, event, app, util, console, display, color, and the document-level scripts (except the PDF container scripts when XFA layout is active; they remain available through js_document_scripts). Field changes and resetForm are applied as one undoable step; returns the script's alerts (each with its message, title, icon and buttons, in order; every question is still answered OK), console output, requests (print, page, url, submit), error, and the errors of document-level scripts that failed (the rest still ran).")
t("js_run", "Run JavaScript", "Run Acrobat JavaScript in the document, as the JavaScript console does (or as push button `field`'s Mouse Up script when field is given; on a laid-out XFA form, `field` runs that button's XFA click script instead, JavaScript or FormCalc, which can add and remove rows and show or hide subforms). The form object model is available: this/getField, event, app, util, console, display, color, and the document-level scripts (except the PDF container scripts when XFA layout is active; they remain available through js_document_scripts). Field changes and resetForm are applied as one undoable step; returns the alerts (each with its message, title, icon and buttons, in order; every question is still answered OK), console output and requests (print, page, url, submit) of the script and of the field scripts its changes ran, the script's error, the errors of document-level scripts that failed (the rest still ran), and in warnings the errors of applying the changes, such as values of an XFA form that could not be written to its data.")
.cmd("tools.js_console")
.with(schema(
json!({ "doc": doc(), "script": { "type": "string" }, "field": { "type": "string", "description": "Run as this button's Mouse Up event." } }),
+88
View File
@@ -9483,6 +9483,94 @@ fn measurement_snap_tool_covers_all_targets() {
let _ = std::fs::remove_dir_all(dir);
}
/// An XFA button click through js_run returns only what the click produced, not the
/// alerts of an earlier fill.
#[test]
fn an_xfa_click_through_js_run_does_not_return_an_earlier_fills_alerts() {
let dir = workdir("xfa-click-output");
std::fs::write(dir.join("scripted.pdf"), pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template())).unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "scripted.pdf" }))["doc"].as_u64().unwrap();
// 500 fails qty's validation, which shows its message.
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "qty": "500" } }));
let r = ok(&mut a, "js_run", json!({ "doc": doc, "script": "", "field": "hello" }));
assert_eq!(r["alerts"], json!([{ "message": "Hello 500", "title": null, "icon": "error", "buttons": "ok" }]), "{r}");
let _ = std::fs::remove_dir_all(dir);
}
/// form_set_script returns only the console lines its own edit produced, not those of an
/// earlier fill.
#[test]
fn form_set_script_does_not_return_an_earlier_fills_console_lines() {
let dir = workdir("set-script-output");
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
for (i, name) in ["a", "b"].into_iter().enumerate() {
let y = 20 + 30 * i;
ok(&mut a, "form_add_field", json!({ "doc": doc, "page": 1, "type": "text", "rect": [20, y, 180, y + 22], "name": name }));
}
ok(&mut a, "form_set_script", json!({ "doc": doc, "field": "a", "event": "validate", "script": "console.println('validated ' + event.value);" }));
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "a": "7" } }));
let r = ok(&mut a, "form_set_script", json!({ "doc": doc, "field": "b", "event": "format", "script": "event.value = event.value;" }));
assert_eq!(r["console"], json!([]), "{r}");
assert_eq!(r["errors"], json!([]), "{r}");
let _ = std::fs::remove_dir_all(dir);
}
/// js_run returns the output of the field scripts that its field changes ran, with the
/// script's own.
#[test]
fn js_run_returns_the_output_of_the_calculations_its_changes_ran() {
let dir = workdir("js-run-calc-output");
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
for (i, name) in ["a", "b"].into_iter().enumerate() {
let y = 20 + 30 * i;
ok(&mut a, "form_add_field", json!({ "doc": doc, "page": 1, "type": "text", "rect": [20, y, 180, y + 22], "name": name }));
}
let calc = "console.println('calculated ' + getField('a').value); event.value = getField('a').value;";
ok(&mut a, "form_set_script", json!({ "doc": doc, "field": "b", "event": "calculate", "script": calc }));
let r = ok(&mut a, "js_run", json!({ "doc": doc, "script": "getField('a').value = '4'; console.println('set');" }));
let console: Vec<&str> = r["console"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
assert!(console.first().is_some_and(|l| l.starts_with("set")), "{r}");
assert!(console.iter().any(|l| l.starts_with("calculated 4")), "{r}");
let _ = std::fs::remove_dir_all(dir);
}
/// The tools that edit a document without going through form_fill's path report the values of
/// an XFA form that could not be written to its data in `warnings`, once, and leave nothing for
/// the next tool to report as its own.
#[test]
fn edits_report_the_xfa_data_they_could_not_write_once() {
let dir = workdir("xfa-data-warnings");
// The data holds structured content where the price goes, so its value can't be written.
let tpl = pdfcraft_xfa::fixtures::scripted_template().replace(
"</xdp:xdp>",
"<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data><form><page1><price><p>7</p></price></page1></form></xfa:data></xfa:datasets>\n</xdp:xdp>",
);
std::fs::write(dir.join("data.pdf"), pdfcraft_xfa::fixtures::shell(&tpl)).unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "data.pdf" }))["doc"].as_u64().unwrap();
let structured = |out: &Value, key: &str| {
out[key].as_array().map_or(0, |w| w.iter().filter(|w| w.as_str().is_some_and(|w| w.contains("structured content"))).count())
};
let r = ok(&mut a, "js_set_document_script", json!({ "doc": doc, "name": "init", "script": "var ready = true;" }));
assert_eq!(structured(&r, "warnings"), 1, "{r}");
assert_eq!(r["scripts"][0]["name"], "init", "{r}");
let r = ok(
&mut a,
"form_set_actions",
json!({ "doc": doc, "field": "hello", "actions": [{ "trigger": "mouse_enter", "javascript": "var entered = true;" }] }),
);
assert_eq!(structured(&r, "warnings"), 1, "{r}");
let r = ok(&mut a, "js_run", json!({ "doc": doc, "script": "getField('qty').value = 3;" }));
assert_eq!(structured(&r, "warnings"), 1, "{r}");
// form_set_script lists its edit's own warning in errors, and the earlier tools' are not repeated.
let r = ok(&mut a, "form_set_script", json!({ "doc": doc, "field": "qty", "event": "format", "script": "event.value = event.value;" }));
assert_eq!(structured(&r, "errors"), 1, "{r}");
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn growable_xfa_text_paginates_after_fill_save_and_print() {
let dir = workdir("xfa-growable-text");
+95 -2
View File
@@ -119,6 +119,49 @@ impl JsOutput {
self.errors.push(e);
}
}
/// Takes out the alerts, console lines, requests and errors. The record of the document
/// scripts' errors stays, so they are still reported once.
fn set_aside(&mut self) -> JsOutput {
JsOutput {
alerts: std::mem::take(&mut self.alerts),
console: std::mem::take(&mut self.console),
requests: std::mem::take(&mut self.requests),
errors: std::mem::take(&mut self.errors),
doc_errors: Vec::new(),
}
}
/// Puts back what [`JsOutput::set_aside`] took out and returns what was added since.
fn put_back(&mut self, waiting: JsOutput) -> JsOutput {
let own = self.set_aside();
self.alerts = waiting.alerts;
self.console = waiting.console;
self.requests = waiting.requests;
self.errors = waiting.errors;
own
}
}
impl Session {
/// Runs `f` and returns its value with the script output `f` added to document `id`. The
/// output limits count that output alone, and the output that was waiting stays for
/// [`Session::take_js_output`]. When `f` fails, its output joins the waiting output.
pub fn with_own_js_output<T, E>(&mut self, id: DocId, f: impl FnOnce(&mut Session) -> Result<T, E>) -> Result<(T, JsOutput), E> {
let waiting = self.docs.iter_mut().find(|d| d.id == id).map(|d| d.js_output.set_aside());
let result = f(self);
let mut doc = self.docs.iter_mut().find(|d| d.id == id);
let own = doc.as_mut().zip(waiting).map(|(d, w)| d.js_output.put_back(w)).unwrap_or_default();
match result {
Ok(value) => Ok((value, own)),
Err(e) => {
if let Some(d) = doc {
d.js_output.append(own);
}
Err(e)
}
}
}
}
fn kind(k: FieldKind) -> pdfcraft_js::FieldType {
@@ -342,8 +385,8 @@ impl Session {
&& let Some(field) = doc.form.iter().find(|f| f.name == t)
&& let Some(som) = crate::xfa::clickable_som(&cos, &tpl, field)
{
self.apply(id, Edit::XfaEvent { som, activity: "click".into() })?;
let out = self.take_js_output(id);
// The click returns only the output of its own edit; output from opening or from earlier edits stays.
let ((), out) = self.with_own_js_output(id, |s| s.apply(id, Edit::XfaEvent { som, activity: "click".into() }))?;
return Ok(Outcome {
rc: true,
alerts: out.alerts,
@@ -570,6 +613,56 @@ mod review_tests {
assert_eq!(reset_targets(&names, &requests), None, "a call without names resets the whole form");
}
/// A click returns its own alert when the output waiting in the document is full, and the
/// waiting output stays.
#[test]
fn a_click_returns_its_alert_when_the_waiting_output_is_full() {
let mut s = Session::new();
let pdf = pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template());
let id = s.open("xfa.pdf", None, std::sync::Arc::new(pdf), None).unwrap();
s.take_js_output(id);
s.docs.iter_mut().find(|d| d.id == id).unwrap().js_output.alert(vec![Message::plain("waiting"); MAX_ALERTS]);
let o = s.run_javascript(id, "", Some("hello")).unwrap();
assert_eq!(o.alerts.iter().map(|a| a.text.as_str()).collect::<Vec<_>>(), ["Hello 2"]);
let out = s.take_js_output(id);
assert_eq!(out.alerts, vec![Message::plain("waiting"); MAX_ALERTS]);
}
/// With the console full and closed, an operation still gets its own lines. Output of an
/// operation that fails joins the waiting output.
#[test]
fn output_limits_count_only_the_operations_own_output() {
let mut s = Session::new();
let pdf = pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template());
let id = s.open("xfa.pdf", None, std::sync::Arc::new(pdf), None).unwrap();
s.take_js_output(id);
fn output(s: &mut Session, id: DocId) -> &mut JsOutput {
&mut s.docs.iter_mut().find(|d| d.id == id).unwrap().js_output
}
output(&mut s, id).say(vec!["waiting".to_string(); MAX_CONSOLE_LINES + 1]);
let waiting = output(&mut s, id).console.clone();
assert!(waiting.last().unwrap().starts_with("more console lines were left out"));
let ((), own) = s
.with_own_js_output::<_, ()>(id, |s| {
output(s, id).say(["mine".to_string()]);
output(s, id).errors.push("own error".into());
Ok(())
})
.unwrap();
assert_eq!(own.console, ["mine"]);
assert_eq!(own.errors, ["own error"]);
assert_eq!(output(&mut s, id).console, waiting);
assert!(output(&mut s, id).errors.is_empty());
let failed = s.with_own_js_output::<(), _>(id, |s| {
output(s, id).errors.push("failed edit".into());
Err("no")
});
assert_eq!(failed.unwrap_err(), "no");
let out = s.take_js_output(id);
assert_eq!(out.console, waiting);
assert_eq!(out.errors, ["failed edit"]);
}
/// A form of many calculated fields, each printing its own cap of console lines, keeps a
/// bounded console: past the budget, lines are left out and one closing line says so.
#[test]
+23
View File
@@ -7933,6 +7933,29 @@ fn xfa_scripts_initialize_calculate_validate_toggle_and_add_rows() {
assert!(!d2.dirty);
}
/// An XFA button's click returns only what the click produced. The alerts and errors of the
/// scripts that ran on open stay in the document's output.
#[test]
fn an_xfa_click_returns_only_its_own_script_output() {
let template = pdfcraft_xfa::fixtures::scripted_template()
.replace("if (qty.rawValue === null) qty.rawValue = 2;", "qty.rawValue = 2; xfa.host.messageBox('opened'); throw new Error('open failed');");
let mut s = Session::new();
let id = s.open("xfa.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&template)), None).unwrap();
let o = s.run_javascript(id, "", Some("hello")).unwrap();
assert_eq!(o.alerts.iter().map(|a| a.text.as_str()).collect::<Vec<_>>(), ["Hello 2"]);
assert!(o.error.is_none(), "{o:?}");
let out = s.take_js_output(id);
assert_eq!(out.alerts.iter().map(|a| a.text.as_str()).collect::<Vec<_>>(), ["opened"]);
assert!(out.errors.iter().any(|e| e.contains("open failed")), "{out:?}");
// A click whose script fails returns that error, and the next click does not repeat it.
let broken =
pdfcraft_xfa::fixtures::scripted_template().replace(r#"xfa.host.messageBox("Hello " + qty.rawValue);"#, "throw new Error('click failed');");
let id = s.open("broken.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&broken)), None).unwrap();
s.take_js_output(id);
assert!(s.run_javascript(id, "", Some("hello")).unwrap().error.is_some_and(|e| e.contains("click failed")));
assert!(s.run_javascript(id, "", Some("legacy")).unwrap().error.is_none());
}
/// A repeating table inside an area of a positioned page (as in real Designer forms): every
/// row is laid out, the values Adobe wrote (under the area's parent: areas are
/// not data scopes) are read, a FormCalc total over all rows computes, and what is filled here