From 96bcdfec1a8d1f8e6e2479f0b6baa749b1914ae9 Mon Sep 17 00:00:00 2001
From: FredApps <153173854+FredApps@users.noreply.github.com>
Date: Wed, 11 Nov 2026 11:24:39 +0100
Subject: [PATCH] feat(automation): js_page and js_advance; parity and docs for
page actions and timers (#169)
Co-authored-by: Peter Yatsyshin
---
crates/automation/src/a11y/form_tools.rs | 66 +++++++++++++++++++
crates/automation/src/dispatch.rs | 2 +
crates/automation/src/tools.rs | 4 ++
crates/automation/tests/it/main.rs | 1 +
crates/automation/tests/it/page_actions.rs | 77 ++++++++++++++++++++++
crates/engine/src/js.rs | 3 +-
crates/engine/src/live_scripts.rs | 7 +-
crates/engine/src/session/view.rs | 6 +-
docs/acroform-script-workflows.md | 30 +++++++++
parity/acrobat-features.toml | 14 ++--
10 files changed, 199 insertions(+), 11 deletions(-)
create mode 100644 crates/automation/tests/it/page_actions.rs
diff --git a/crates/automation/src/a11y/form_tools.rs b/crates/automation/src/a11y/form_tools.rs
index 31234a28ed..30d92f884b 100644
--- a/crates/automation/src/a11y/form_tools.rs
+++ b/crates/automation/src/a11y/form_tools.rs
@@ -66,6 +66,59 @@ impl Automation {
))
}
+ /// `js_page`: read the document at a page, running page actions as the viewer does (#169).
+ pub(crate) fn js_page(&mut self, a: &Args) -> Result {
+ let id = self.doc(a)?.id;
+ let page = self.page(a)?;
+ let clock = self.session.script_clock(id).unwrap_or(0.0);
+ let run = self.session.show_page(id, page, clock);
+ // What the actions showed or hid is in the document's view settings.
+ self.renderers.remove(&id);
+ Ok(self.script_run_json(id, &run))
+ }
+
+ /// `js_advance`: move the script clock on, running the timers that fall due on the way, in
+ /// the order they do.
+ pub(crate) fn js_advance(&mut self, a: &Args) -> Result {
+ /// Most timer runs one call makes (a 10 ms interval for a minute).
+ const MAX_RUNS: usize = 6000;
+ let id = self.doc(a)?.id;
+ let ms = a.opt_num("ms")?.ok_or_else(|| ToolError::InvalidArgs("ms is required".into()))?;
+ if !(0.0..=3_600_000.0).contains(&ms) {
+ return Err(ToolError::InvalidArgs("ms must be between 0 and 3600000".into()));
+ }
+ let start = self.session.script_clock(id).unwrap_or(0.0);
+ let end = start + ms;
+ let mut total = pdfcraft_engine::ScriptRun::default();
+ while total.ran < MAX_RUNS {
+ let Some(at) = self.session.next_timer(id).filter(|at| *at <= end) else { break };
+ let run = self.session.run_timers(id, at.max(start));
+ if run.ran == 0 {
+ break;
+ }
+ merge_runs(&mut total, run);
+ }
+ let behind = self.session.next_timer(id).is_some_and(|at| at <= end);
+ // The clock ends where it was sent; a timer the run cap left behind counts on from there.
+ let last = self.session.run_timers(id, end);
+ merge_runs(&mut total, last);
+ self.renderers.remove(&id);
+ let mut out = self.script_run_json(id, &total);
+ out["ran"] = json!(total.ran);
+ out["stopped_early"] = json!(behind);
+ Ok(out)
+ }
+
+ fn script_run_json(&mut self, id: pdfcraft_engine::DocId, run: &pdfcraft_engine::ScriptRun) -> Value {
+ let reqs: Vec = run.output.requests.iter().map(request_json).collect();
+ let alerts: Vec = run.output.alerts.iter().map(alert_json).collect();
+ let pages: Vec = run.pages.iter().map(|p| p + 1).collect();
+ let clock = self.session.script_clock(id).unwrap_or(0.0);
+ let next = self.session.next_timer(id).map(|at| (at - clock).max(0.0));
+ json!({ "alerts": alerts, "console": run.output.console, "requests": reqs, "errors": run.output.errors,
+ "pages": pages, "edited": run.edited, "layers": run.layers, "clock_ms": clock, "next_timer_in_ms": next })
+ }
+
pub(crate) fn js_document_scripts(&self, a: &Args) -> Result {
let list: Vec = self.doc(a)?.document_scripts().into_iter().map(|(n, s)| json!({ "name": n, "script": s })).collect();
Ok(json!({ "scripts": list }))
@@ -236,3 +289,16 @@ impl Automation {
Ok(json!({ "enabled": self.session.javascript() }))
}
}
+
+/// Adds what `run` did to `total`.
+fn merge_runs(total: &mut pdfcraft_engine::ScriptRun, run: pdfcraft_engine::ScriptRun) {
+ total.ran += run.ran;
+ total.edited |= run.edited;
+ total.layers |= run.layers;
+ total.output.append(run.output);
+ for p in run.pages {
+ if !total.pages.contains(&p) {
+ total.pages.push(p);
+ }
+ }
+}
diff --git a/crates/automation/src/dispatch.rs b/crates/automation/src/dispatch.rs
index 9644684772..8947d6a034 100644
--- a/crates/automation/src/dispatch.rs
+++ b/crates/automation/src/dispatch.rs
@@ -835,6 +835,8 @@ impl Automation {
"accessibility_autotag_form_fields" => self.accessibility_autotag_form_fields(&a)?,
"ocr_recognize" => self.ocr_recognize(&a)?,
"js_run" => self.js_run(&a)?,
+ "js_page" => self.js_page(&a)?,
+ "js_advance" => self.js_advance(&a)?,
"js_document_scripts" => self.js_document_scripts(&a)?,
"js_set_document_script" => self.js_set_document_script(&a)?,
"js_enabled" => self.js_enabled(&a)?,
diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs
index 83362aa019..152e10398b 100644
--- a/crates/automation/src/tools.rs
+++ b/crates/automation/src/tools.rs
@@ -1421,6 +1421,10 @@ pub fn tools() -> Vec {
json!({ "doc": doc(), "script": { "type": "string" }, "field": { "type": "string", "description": "Run as this button's Mouse Up event." }, "field_id": {"type":"integer","minimum":0,"maximum":281474976710655u64,"description":"Optional exact field identity from form_fields; requires matching field name and is scoped to this document."}, "answers": {"type":"array","maxItems":32,"items":{"type":["string","null"],"maxLength":4096},"description":"Ordered popup, response and alert answers; strings are limited to 4096 UTF-8 bytes at runtime; null cancels a popup or response, and selects an alert default (OK, Cancel or No, by button set)."} }),
&["doc", "script"],
)),
+ t("js_page", "Run page actions", "Read the document at `page`, as the viewer does when that page becomes the one being read: the actions the page it was at runs as it closes, then this page's open actions (its own, and its annotations' page-open and page-visible actions, which start an animation made with the LaTeX animate package), in the document's JavaScript context, which buttons, the console and timers share. Fields the actions show or hide change only how the document is shown, and page_render draws them so; values they change are applied as one undoable step. Returns the actions' alerts, console output, requests and errors, the pages showing a field they showed or hid, and when the next timer falls due on the script clock (js_advance moves it).")
+ .with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])),
+ t("js_advance", "Advance the script clock", "Move the document's script clock on by `ms` milliseconds and run the timers its scripts set (app.setInterval, app.setTimeOut) as they fall due on the way, in order, as the viewer runs them over time: an animation shows its next frames. Fields timers show or hide change only how the document is shown, and page_render draws them so. A timer's alerts and requests go to its console output instead. Returns how many timers ran, their console output and errors, the pages showing a field they showed or hid, the clock, and when the next timer falls due.")
+ .with(schema(json!({ "doc": doc(), "ms": { "type": "number", "minimum": 0, "maximum": 3600000 } }), &["doc", "ms"])),
t("js_document_scripts", "Document JavaScripts", "List the document-level JavaScripts (name and source), which define functions field scripts use.")
.ro()
.cmd("tools.document_js")
diff --git a/crates/automation/tests/it/main.rs b/crates/automation/tests/it/main.rs
index 7a5a6c1bae..42fd5459a3 100644
--- a/crates/automation/tests/it/main.rs
+++ b/crates/automation/tests/it/main.rs
@@ -15,6 +15,7 @@ mod hostile_args;
mod list_selection;
mod model_3d;
mod model_render;
+mod page_actions;
mod optimize_content_boundaries;
mod portfolio_info;
mod redact_after_stray_q_form;
diff --git a/crates/automation/tests/it/page_actions.rs b/crates/automation/tests/it/page_actions.rs
new file mode 100644
index 0000000000..7cdc62b8d9
--- /dev/null
+++ b/crates/automation/tests/it/page_actions.rs
@@ -0,0 +1,77 @@
+//! Page actions and timers through the tool table (#169): `js_page` reads a page as the viewer
+//! does, `js_advance` moves the script clock on, and `page_render` draws what they show.
+
+use pdfcraft_automation::{Automation, Content};
+use serde_json::{Value, json};
+
+fn tool(a: &mut Automation, name: &str, args: Value) -> Value {
+ match a.call(name, &args).unwrap().remove(0) {
+ Content::Json(value) => value,
+ other => panic!("expected JSON: {other:?}"),
+ }
+}
+
+/// Two pages; on the first, two frames over one another (red showing, green hidden) and a
+/// page-open action flipping them every 100 ms, with a page-close action stopping it.
+fn animation_pdf() -> Vec {
+ let fill = |rgb: &str| {
+ let body = format!("{rgb} rg 0 0 100 100 re f");
+ format!("<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] /Length {} >>\nstream\n{body}\nendstream", body.len())
+ };
+ let open = "var on = 0; function flip() { getField('f' + on).display = display.hidden; on = 1 - on; getField('f' + on).display = display.visible; } \
+ var t = app.setInterval('flip()', 100);";
+ let objs: Vec = vec![
+ "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R] >> >>".into(),
+ "<< /Type /Pages /Kids [3 0 R 10 0 R] /Count 2 /MediaBox [0 0 200 300] >>".into(),
+ "<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] /AA << /O << /S /JavaScript /JS 8 0 R >> /C << /S /JavaScript /JS 9 0 R >> >> >>".into(),
+ "<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (f0) /F 4 /Rect [0 0 200 300] /P 3 0 R /AP << /N 6 0 R >> >>".into(),
+ "<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (f1) /F 6 /Rect [0 0 200 300] /P 3 0 R /AP << /N 7 0 R >> >>".into(),
+ fill("1 0 0"),
+ fill("0 1 0"),
+ format!("<< /Length {} >>\nstream\n{open}\nendstream", open.len()),
+ "<< /Length 20 >>\nstream\napp.clearInterval(t)\nendstream".into(),
+ "<< /Type /Page /Parent 2 0 R >>".into(),
+ ];
+ let mut out = b"%PDF-1.7\n".to_vec();
+ let mut offsets = Vec::new();
+ for (i, o) in objs.iter().enumerate() {
+ offsets.push(out.len());
+ out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
+ }
+ let xref = out.len();
+ out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
+ for o in offsets {
+ out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
+ }
+ out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
+ out
+}
+
+#[test]
+fn js_page_and_js_advance_run_an_animation_that_page_render_draws() {
+ let dir = std::env::temp_dir().join(format!("pdfcraft-page-actions-{}", std::process::id()));
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join("anim.pdf"), animation_pdf()).unwrap();
+ let mut a = Automation::new().with_root(&dir).unwrap();
+ let id = tool(&mut a, "doc_open", json!({ "path": "anim.pdf" }))["doc"].as_u64().unwrap();
+ let centre = |a: &mut Automation| {
+ let (w, h, px) = a.render_pixels(id, 1, 1.0).unwrap();
+ let i = ((h / 2 * w + w / 2) * 4) as usize;
+ px[i..i + 3].to_vec()
+ };
+ assert_eq!(centre(&mut a), [255, 0, 0]);
+ let out = tool(&mut a, "js_page", json!({ "doc": id, "page": 1 }));
+ assert_eq!((out["errors"].clone(), out["next_timer_in_ms"].clone()), (json!([]), json!(100.0)), "{out}");
+ let out = tool(&mut a, "js_advance", json!({ "doc": id, "ms": 150 }));
+ assert_eq!((out["ran"].clone(), out["pages"].clone(), out["edited"].clone()), (json!(1), json!([1]), json!(false)), "{out}");
+ assert_eq!(out["next_timer_in_ms"], json!(50.0));
+ assert_eq!(centre(&mut a), [0, 255, 0], "page_render draws the frame the timer showed");
+ let out = tool(&mut a, "js_advance", json!({ "doc": id, "ms": 1000 }));
+ assert_eq!(out["ran"], json!(10), "{out}");
+ assert_eq!(centre(&mut a), [0, 255, 0]);
+ assert!(a.call("js_advance", &json!({ "doc": id, "ms": 3_600_001 })).is_err());
+ let out = tool(&mut a, "js_page", json!({ "doc": id, "page": 2 }));
+ assert_eq!(out["next_timer_in_ms"], Value::Null, "the page's close action cleared the interval: {out}");
+ let doc = a.session().get(pdfcraft_engine::DocId(id)).unwrap();
+ assert!(!doc.dirty && doc.undo_depth() == 0, "the frames turned in the view only");
+}
diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs
index fa63da67be..fb431966fd 100644
--- a/crates/engine/src/js.rs
+++ b/crates/engine/src/js.rs
@@ -187,7 +187,8 @@ impl JsOutput {
}
}
- pub(crate) fn append(&mut self, other: JsOutput) {
+ /// Adds what other scripts produced, telling a document script's error once.
+ pub fn append(&mut self, other: JsOutput) {
self.alert(other.alerts);
self.say(other.console);
self.requests.extend(other.requests);
diff --git a/crates/engine/src/live_scripts.rs b/crates/engine/src/live_scripts.rs
index bbf89853ff..0b70771b5a 100644
--- a/crates/engine/src/live_scripts.rs
+++ b/crates/engine/src/live_scripts.rs
@@ -122,6 +122,11 @@ fn object(w: &pdfcraft_forms::Widget) -> Option<(i32, i32)> {
/// The viewer's render configuration: the document's, with installed fonts, and the fields as
/// page actions and timers show them. Exports and printing use `doc.config`, without them.
pub(crate) fn screen_config(doc: &Document) -> pdfcraft_render::RenderConfig {
+ with_live_view(doc, crate::display_config(&doc.config))
+}
+
+/// `config` with the fields as page actions and timers show them.
+pub(crate) fn with_live_view(doc: &Document, config: pdfcraft_render::RenderConfig) -> pdfcraft_render::RenderConfig {
let live = &doc.live;
let annotations: Vec<(i32, i32, bool)> = doc
.form
@@ -143,7 +148,7 @@ pub(crate) fn screen_config(doc: &Document) -> pdfcraft_render::RenderConfig {
pdfcraft_render::RenderConfig {
annotations: std::sync::Arc::new(annotations),
appearances: std::sync::Arc::new(appearances),
- ..crate::display_config(&doc.config)
+ ..config
}
}
diff --git a/crates/engine/src/session/view.rs b/crates/engine/src/session/view.rs
index 7232f64631..a519fb7e76 100644
--- a/crates/engine/src/session/view.rs
+++ b/crates/engine/src/session/view.rs
@@ -336,9 +336,11 @@ impl Session {
}
/// How the document's pages are drawn for viewing: its password and view settings (layer
- /// visibility, hidden comments, reading colours). `None` when no such document is open.
+ /// visibility, hidden comments, reading colours), and the fields as its page actions and
+ /// timers show them (#169). `None` when no such document is open.
pub fn render_config(&self, id: DocId) -> Option {
- Some(self.get(id)?.config.clone())
+ let doc = self.get(id)?;
+ Some(crate::live_scripts::with_live_view(doc, doc.config.clone()))
}
/// Acrobat's Summarize Comments ("Comments only" layout): a new PDF listing every comment
diff --git a/docs/acroform-script-workflows.md b/docs/acroform-script-workflows.md
index 200a59966a..5d4059f43e 100644
--- a/docs/acroform-script-workflows.md
+++ b/docs/acroform-script-workflows.md
@@ -83,8 +83,38 @@ host path cancels unanswered popups and response dialogs; no web-interaction par
This work does not add timer execution, PDF annotations/layers to the JS object model, or the
remaining document action triggers.
+## Page actions, timers and layers
+
+When the page being read changes (the active tab's current page), the close actions of the
+page it was at run, then the new page's open actions: the page's own `/AA /O` and `/C`, and
+each annotation's `/PO`, `/PV` (opening) and `/PC`, `/PI` (closing), in `/Annots` order. They
+run in the document realm, so the functions and variables they define are there for buttons,
+the console and timers. This is how LaTeX `animate` animations start and stop.
+
+`app.setInterval`, `app.setTimeOut`, `app.clearInterval` and `app.clearTimeOut` keep up to 64
+timers per realm, each at least 10 ms apart. The viewer fires those that are due each frame (at
+most 16 per call; an interval that fell behind runs once and then counts from now). A timer
+runs unasked, so its alerts and requests go to the console. A fresh realm (after an edit
+resets it, Revert, or a full save) has no timers.
+
+Fields that page actions and timers show, hide or give another icon (`display`,
+`buttonSetIcon`) change only how the document is shown, through view-only render overrides
+(hayro-interpret patches 104 and 105). They aren't saved, printed or exported, and they don't
+mark the document as changed. Buttons and the console see the fields as shown, and a `display`
+or icon a button writes into the document takes over from the view's. A value or a reset from
+a page action or timer is one undoable edit.
+
+`this.getOCGs()` lists the document's layers (`name`, `state`, `initState`); setting
+`ocg.state` turns a layer as the Layers panel does.
+
## Automation
+`js_page` reads the document at a page and runs its page actions as the viewer does, and
+`js_advance` moves the script clock on by `ms` (up to one hour) and runs the timers that fall
+due on the way (at most 6,000 runs per call). Both return the scripts' output, the pages to
+draw again, and when the next timer is due; `page_render` draws the fields as the scripts show
+them.
+
The existing `js_run` accepts `answers`, an optional array of up to 32 strings or null values:
```json
diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml
index a5e052341a..f481fc1f11 100644
--- a/parity/acrobat-features.toml
+++ b/parity/acrobat-features.toml
@@ -3295,11 +3295,11 @@ title = "Page open/close actions"
area = "D"
tier = "P2"
milestone = "M6"
-status = "planned"
+status = "partial"
commands = []
-tools = []
-evidence = []
-notes = ""
+tools = ["js_page"]
+evidence = ["crates/forms/src/tests.rs::page_scripts_are_the_pages_own_then_each_annotations_in_order", "crates/engine/src/live_scripts_tests.rs::page_actions_start_timers_that_turn_an_animations_frames_without_changing_the_document", "crates/automation/tests/it/page_actions.rs::js_page_and_js_advance_run_an_animation_that_page_render_draws"]
+notes = "JavaScript page actions run in the document's script realm when the page being read changes (the active tab's current page): the page's own open and close actions (/AA /O, /C) and its annotations' page-open, page-visible, page-close and page-invisible actions (/PO, /PV, /PC, /PI), which start and stop LaTeX animate animations. Fields they show, hide or give another icon change only how the document is shown (not saved, printed or exported); values they change are one undoable edit. Not yet: non-JavaScript page actions, actions chained with /Next, and the page-visible/invisible distinction for pages scrolled into view without becoming current."
[[feature]]
id = "organize.page-templates"
@@ -5362,9 +5362,9 @@ tier = "P0"
milestone = "M6"
status = "partial"
commands = ["tools.js_console"]
-tools = ["js_run"]
-evidence = ["crates/automation/tests/it/timesheet_compat.rs::timesheet_keypad_geometry_icons_calculation_and_persistence", "crates/automation/tests/it/timesheet_compat.rs::popup_and_manual_response_resume_the_same_script_and_validate_choices", "crates/js/src/session.rs::document_realm_keeps_keypad_state_and_isolates_documents", "crates/js/src/tests.rs::calculate_scripts_read_fields_and_set_the_value", "crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats", "crates/automation/tests/it/automation.rs::javascript_through_tools", "crates/js/src/tests.rs::an_xfa_form_sees_the_xfa_plugin_installed", "crates/js/src/tests.rs::a_plain_form_sees_the_xfa_version_but_not_installed", "crates/engine/src/tests.rs::scripts_in_an_xfa_form_see_the_xfa_plugin_installed", "crates/js/src/tests.rs::scand_allows_separators_between_fields_but_nothing_before_or_after_them", "crates/js/src/tests.rs::app_alert_keeps_its_title_icon_and_buttons", "crates/js/src/tests.rs::app_alert_reads_the_object_form", "crates/js/src/tests.rs::app_alert_falls_back_on_bad_icon_and_button_numbers", "crates/js/src/tests.rs::app_alert_titles_are_clipped_and_charged", "crates/ui-egui/tests/it/javascript.rs::every_message_a_script_shows_takes_the_toast_in_turn", "crates/ui-egui/tests/it/forms.rs::a_form_button_opens_its_attached_instructions_in_a_new_tab", "crates/js/src/tests.rs::widget_rect_reads_and_writes_a_four_number_array", "crates/engine/src/tests.rs::javascript_field_rect_moves_first_widget_and_undoes"]
-notes = "Document-scoped native Boa realms shared by button, console and field events; synchronous calculateNow in CO order; normal captions/icons; rect and appearance/tooltip writeback; effective page boxes; native explicit-button popup/response/alert continuation and viewer requests, including opening or saving attachments from form buttons. Headless js_run has deterministic ordered answers. Partial: web prompt UI, timers, annotation/layer APIs, non-normal button faces, widget-indexed Field access and remaining document action triggers are not implemented. See docs/acroform-script-workflows.md for lifecycle, limits and validation scope."
+tools = ["js_run", "js_advance"]
+evidence = ["crates/automation/tests/it/timesheet_compat.rs::timesheet_keypad_geometry_icons_calculation_and_persistence", "crates/automation/tests/it/timesheet_compat.rs::popup_and_manual_response_resume_the_same_script_and_validate_choices", "crates/js/src/session.rs::document_realm_keeps_keypad_state_and_isolates_documents", "crates/js/src/tests.rs::calculate_scripts_read_fields_and_set_the_value", "crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats", "crates/automation/tests/it/automation.rs::javascript_through_tools", "crates/js/src/tests.rs::an_xfa_form_sees_the_xfa_plugin_installed", "crates/js/src/tests.rs::a_plain_form_sees_the_xfa_version_but_not_installed", "crates/engine/src/tests.rs::scripts_in_an_xfa_form_see_the_xfa_plugin_installed", "crates/js/src/tests.rs::scand_allows_separators_between_fields_but_nothing_before_or_after_them", "crates/js/src/tests.rs::app_alert_keeps_its_title_icon_and_buttons", "crates/js/src/tests.rs::app_alert_reads_the_object_form", "crates/js/src/tests.rs::app_alert_falls_back_on_bad_icon_and_button_numbers", "crates/js/src/tests.rs::app_alert_titles_are_clipped_and_charged", "crates/ui-egui/tests/it/javascript.rs::every_message_a_script_shows_takes_the_toast_in_turn", "crates/ui-egui/tests/it/forms.rs::a_form_button_opens_its_attached_instructions_in_a_new_tab", "crates/js/src/tests.rs::widget_rect_reads_and_writes_a_four_number_array", "crates/engine/src/tests.rs::javascript_field_rect_moves_first_widget_and_undoes", "crates/js/src/session.rs::timers_fire_in_the_document_realm_until_cleared", "crates/engine/src/live_scripts_tests.rs::a_timer_giving_a_button_another_icon_draws_that_icon_in_its_place", "crates/engine/src/live_scripts_tests.rs::get_ocgs_turns_a_layer_in_the_view"]
+notes = "Document-scoped native Boa realms shared by button, console and field events; synchronous calculateNow in CO order; normal captions/icons; rect and appearance/tooltip writeback; effective page boxes; native explicit-button popup/response/alert continuation and viewer requests, including opening or saving attachments from form buttons. Headless js_run has deterministic ordered answers. Partial: web prompt UI, timers, annotation/layer APIs, non-normal button faces, widget-indexed Field access and remaining document action triggers are not implemented. See docs/acroform-script-workflows.md for lifecycle, limits and validation scope. app.setInterval/setTimeOut/clearInterval/clearTimeOut (at most 64 timers, 10 ms minimum) fire in the document realm on the viewer clock (js_advance headless); timer alerts and requests go to the console. this.getOCGs() with ocg.name/state/initState turns layers as the Layers panel does."
[[feature]]
id = "form.js-console"