1use super::*;
8use crate::browser::policy::PolicyCapability;
9use std::time::Duration;
10
11impl BrowserSession {
12 pub async fn run_batch(&self, steps: &[BatchStep]) -> BrowserResult<BatchOutcome> {
16 self.run_batch_with_options(steps, false).await
17 }
18
19 pub async fn run_batch_with_options(
21 &self,
22 steps: &[BatchStep],
23 atomic: bool,
24 ) -> BrowserResult<BatchOutcome> {
25 let total = steps.len();
26 if total > MAX_BATCH_STEPS {
27 return Err(format!(
28 "batch exceeds max {} steps (received {total})",
29 MAX_BATCH_STEPS
30 )
31 .into());
32 }
33
34 for (index, step) in steps.iter().enumerate() {
35 let action = step_action_name(step);
36 if let Err(policy_error) = check_batch_step_policy(self, step).await {
37 return Err(format!(
38 "batch policy denial at step {index} ({action}): {policy_error}"
39 )
40 .into());
41 }
42 }
43
44 if atomic {
45 for (index, step) in steps.iter().enumerate() {
46 if let Some(target) = batch_target(step) {
47 self.resolve_element(target).await.map_err(|error| {
48 format!("atomic batch target resolution failed at step {index}: {error}")
49 })?;
50 }
51 }
52 }
53
54 let mut outcomes: Vec<BatchStepOutcome> = Vec::with_capacity(total);
55 let mut completed = 0usize;
56 let mut failed = 0usize;
57
58 for (index, step) in steps.iter().enumerate() {
59 let action = step_action_name(step);
60 match execute_batch_step(self, step).await {
61 Ok(response_bytes) => {
62 outcomes.push(BatchStepOutcome::Success {
63 index,
64 action,
65 response_bytes,
66 });
67 completed += 1;
68 }
69 Err(error) => {
70 let message = bounded_batch_text(&format!("{error:#}"), 512);
71 outcomes.push(BatchStepOutcome::Error {
72 index,
73 action,
74 message,
75 });
76 failed += 1;
77 break;
78 }
79 }
80 }
81
82 Ok(BatchOutcome {
83 success: failed == 0,
84 completed,
85 failed,
86 total,
87 steps: outcomes,
88 })
89 }
90}
91
92async fn check_batch_step_policy(
93 session: &BrowserSession,
94 step: &BatchStep,
95) -> Result<(), PolicyError> {
96 match step {
97 BatchStep::Navigate { url, .. } => session.policy.require_url(url).await.map(|_| ()),
98 BatchStep::Evaluate { .. } => session.policy.require_for_batch(PolicyCapability::Evaluate),
99 BatchStep::Observe {
100 include_form_values,
101 ..
102 } => {
103 if *include_form_values {
104 session
105 .policy
106 .require_for_batch(PolicyCapability::ReadFormValues)
107 } else {
108 Ok(())
109 }
110 }
111 BatchStep::Screenshot => session
112 .policy
113 .require_for_batch(PolicyCapability::Screenshot),
114 _ => Ok(()),
115 }
116}
117
118fn batch_target(step: &BatchStep) -> Option<&str> {
119 match step {
120 BatchStep::Click { target }
121 | BatchStep::Check { target }
122 | BatchStep::Uncheck { target }
123 | BatchStep::Select { target, .. }
124 | BatchStep::Clear { target } => Some(target),
125 BatchStep::Type { target, .. } => target.as_deref(),
126 _ => None,
127 }
128}
129
130fn bounded_batch_text(value: &str, max_bytes: usize) -> String {
131 if value.len() <= max_bytes {
132 return value.to_string();
133 }
134 let mut end = max_bytes.saturating_sub(15);
135 while end > 0 && !value.is_char_boundary(end) {
136 end -= 1;
137 }
138 format!("{}…[truncated]", &value[..end])
139}
140
141async fn execute_batch_step(
142 session: &BrowserSession,
143 step: &BatchStep,
144) -> BrowserResult<Option<usize>> {
145 match step {
146 BatchStep::Navigate { url, timeout_ms } => {
147 let info = session
148 .navigate_with_deadline(url, Duration::from_millis(*timeout_ms))
149 .await?;
150 let bytes = serde_json::to_string(&info).ok().map(|s| s.len());
151 Ok(bytes)
152 }
153 BatchStep::Click { target } => {
154 let outcome = session.click(target).await?;
155 let bytes = serde_json::to_string(&outcome).ok().map(|s| s.len());
156 Ok(bytes)
157 }
158 BatchStep::Type { text, target } => {
159 session.type_text(text, target.as_deref()).await?;
160 Ok(None)
161 }
162 BatchStep::Check { target } => {
163 session.check(target).await?;
164 Ok(None)
165 }
166 BatchStep::Uncheck { target } => {
167 session.uncheck(target).await?;
168 Ok(None)
169 }
170 BatchStep::Select { target, value } => {
171 session.select_option(target, value).await?;
172 Ok(None)
173 }
174 BatchStep::Clear { target } => {
175 session.clear(target).await?;
176 Ok(None)
177 }
178 BatchStep::Scroll { dx, dy } => {
179 session.scroll(*dx, *dy).await?;
180 Ok(None)
181 }
182 BatchStep::Wait {
183 condition,
184 timeout_ms,
185 } => {
186 let cond = WaitCondition::parse(condition)?;
187 session
188 .wait(cond, Duration::from_millis(*timeout_ms))
189 .await?;
190 Ok(None)
191 }
192 BatchStep::Observe {
193 include_dom,
194 include_screenshot,
195 include_form_values,
196 } => {
197 let context = match (*include_dom, *include_screenshot, *include_form_values) {
198 (false, false, false) => session.observe().await?,
199 (true, false, false) => session.observe_with_dom().await?,
200 (false, true, false) => session.observe_with_screenshot().await?,
201 (true, true, false) => session.observe_with_dom_and_screenshot().await?,
202 (false, false, true) => session.observe_with_form_values().await?,
203 _ => {
204 return Err(
205 "form values may only be combined with default compact observe".into(),
206 );
207 }
208 };
209 let bytes = serde_json::to_string(&context).ok().map(|s| s.len());
210 Ok(bytes)
211 }
212 BatchStep::Screenshot => {
213 let data = session.screenshot_base64().await?;
214 Ok(Some(data.len()))
215 }
216 BatchStep::Evaluate { expression } => {
217 let result = session.evaluate(expression).await?;
218 let bytes = serde_json::to_string(&result).ok().map(|s| s.len());
219 Ok(bytes)
220 }
221 BatchStep::AcceptDialog => {
222 session.accept_dialog().await?;
223 Ok(None)
224 }
225 BatchStep::DismissDialog => {
226 session.dismiss_dialog().await?;
227 Ok(None)
228 }
229 }
230}
231
232fn step_action_name(step: &BatchStep) -> String {
233 match step {
234 BatchStep::Navigate { .. } => "navigate".to_string(),
235 BatchStep::Click { .. } => "click".to_string(),
236 BatchStep::Type { .. } => "type".to_string(),
237 BatchStep::Check { .. } => "check".to_string(),
238 BatchStep::Uncheck { .. } => "uncheck".to_string(),
239 BatchStep::Select { .. } => "select".to_string(),
240 BatchStep::Clear { .. } => "clear".to_string(),
241 BatchStep::Scroll { .. } => "scroll".to_string(),
242 BatchStep::Wait { .. } => "wait".to_string(),
243 BatchStep::Observe { .. } => "observe".to_string(),
244 BatchStep::Screenshot => "screenshot".to_string(),
245 BatchStep::Evaluate { .. } => "evaluate".to_string(),
246 BatchStep::AcceptDialog => "acceptDialog".to_string(),
247 BatchStep::DismissDialog => "dismissDialog".to_string(),
248 }
249}