1use super::*;
8
9impl BrowserSession {
10 pub async fn text(&self) -> BrowserResult<String> {
15 self.cdp
16 .with_current_route(async {
17 let value = self
18 .evaluate_value("document.body ? document.body.innerText : ''")
19 .await?;
20 Ok(truncate_visible_text(
21 value.as_str().unwrap_or_default(),
22 COMPACT_TEXT_MAX_BYTES,
23 ))
24 })
25 .await
26 }
27
28 pub async fn deep_dom(&self) -> BrowserResult<DomNode> {
30 self.cdp
31 .with_current_route(async {
32 let raw = self.cdp.get_deep_document().await?;
33 parse_dom_tree(&raw).ok_or_else(|| {
34 "CDP deep DOM response contained no parseable root node"
35 .to_string()
36 .into()
37 })
38 })
39 .await
40 }
41
42 pub async fn observe(&self) -> BrowserResult<PageContext> {
44 self.observe_internal(false, false, true, false, CompactRanking::Relevance)
45 .await
46 }
47
48 pub async fn observe_with_dom(&self) -> BrowserResult<PageContext> {
50 self.observe_internal(true, false, true, false, CompactRanking::Relevance)
51 .await
52 }
53
54 pub async fn observe_with_screenshot(&self) -> BrowserResult<PageContext> {
56 self.observe_internal(false, true, true, false, CompactRanking::Relevance)
57 .await
58 }
59
60 pub async fn observe_with_dom_and_screenshot(&self) -> BrowserResult<PageContext> {
62 self.observe_internal(true, true, true, false, CompactRanking::Relevance)
63 .await
64 }
65
66 pub async fn observe_fresh(&self) -> BrowserResult<PageContext> {
68 self.observe_internal(false, false, false, false, CompactRanking::Relevance)
69 .await
70 }
71
72 pub async fn observe_with_form_values(&self) -> BrowserResult<PageContext> {
75 self.observe_internal(false, false, false, true, CompactRanking::Relevance)
76 .await
77 }
78
79 pub async fn observe_fresh_with_dom(&self) -> BrowserResult<PageContext> {
81 self.observe_internal(true, false, false, false, CompactRanking::Relevance)
82 .await
83 }
84
85 pub async fn observe_fresh_with_screenshot(&self) -> BrowserResult<PageContext> {
87 self.observe_internal(false, true, false, false, CompactRanking::Relevance)
88 .await
89 }
90
91 pub async fn observe_fresh_with_dom_and_screenshot(&self) -> BrowserResult<PageContext> {
93 self.observe_internal(true, true, false, false, CompactRanking::Relevance)
94 .await
95 }
96
97 pub async fn observe_with_ranking(
99 &self,
100 ranking: ObservationRanking,
101 ) -> BrowserResult<PageContext> {
102 let ranking = match ranking {
103 ObservationRanking::Relevance => CompactRanking::Relevance,
104 ObservationRanking::DocumentOrder => CompactRanking::DocumentOrder,
105 };
106 self.observe_internal(false, false, false, false, ranking)
107 .await
108 }
109
110 async fn observe_internal(
111 &self,
112 include_dom: bool,
113 include_screenshot: bool,
114 use_cache: bool,
115 include_form_values: bool,
116 ranking: CompactRanking,
117 ) -> BrowserResult<PageContext> {
118 if let Some(interception) = &self.policy_interception
119 && let Some(error) = interception.take_denial().await
120 {
121 return Err(error.into());
122 }
123 self.cdp
124 .with_current_route(async {
125 let mut context = self
126 .compact_observation(use_cache, include_form_values, ranking)
127 .await?
128 .into_page_context();
129 if include_dom {
130 context.dom = Some(self.deep_dom().await?);
131 }
132 if include_screenshot {
133 context.screenshot = Some(self.screenshot_base64().await?);
134 }
135 Ok(context)
136 })
137 .await
138 }
139
140 async fn compact_observation(
141 &self,
142 use_cache: bool,
143 include_form_values: bool,
144 ranking: CompactRanking,
145 ) -> BrowserResult<CompactPageContext> {
146 let revision = self.page_revision.load(Ordering::Relaxed);
147 if use_cache && !include_form_values {
149 let cached_context = {
150 let cache = self.observation_cache.lock().await;
151 cache
152 .as_ref()
153 .filter(|cached| cached.revision == revision)
154 .map(|cached| cached.context.clone())
155 };
156 if let Some(context) = cached_context {
157 return Ok(context);
158 }
159 }
160
161 let (target_id, frame_id) = self.route_identity().await?;
162 let mut context_id = self.observation_context_id(&target_id, &frame_id).await?;
163 let mut collected = None;
164 let mut attempt = 1;
165 let mut recovered_context = false;
166 while attempt <= COMPACT_OBSERVATION_MAX_ATTEMPTS {
167 let start_revision = self.page_revision.load(Ordering::Relaxed);
168 let attempt_result =
169 match tokio::time::timeout(COMPACT_OBSERVATION_ATTEMPT_TIMEOUT, async {
170 let start = self.compact_page_state(context_id).await?;
171 let accessibility = self.cdp.get_accessibility_tree().await?;
172 let end = self.compact_page_state(context_id).await?;
173 BrowserResult::Ok((start, accessibility, end))
174 })
175 .await
176 {
177 Err(_) => {
178 return Err(
179 "compact observation attempt exceeded its one-second deadline".into(),
180 );
181 }
182 Ok(Ok(result)) => result,
183 Ok(Err(error))
184 if is_stale_observation_context(error.as_ref()) && !recovered_context =>
185 {
186 self.discard_observation_context(&target_id, &frame_id, context_id)
187 .await;
188 context_id = self.observation_context_id(&target_id, &frame_id).await?;
189 recovered_context = true;
190 continue;
191 }
192 Ok(Err(error)) => return Err(error),
193 };
194 let end_revision = self.page_revision.load(Ordering::Relaxed);
195 let consistent = start_revision == end_revision
196 && attempt_result.0.mutation_revision == attempt_result.2.mutation_revision;
197 collected = Some((
198 attempt,
199 consistent,
200 start_revision,
201 end_revision,
202 attempt_result,
203 ));
204 if consistent {
205 break;
206 }
207 attempt += 1;
208 }
209 let (
210 attempts,
211 consistent,
212 start_revision,
213 end_revision,
214 (start_state, accessibility_raw, page_state),
215 ) = collected.expect("observation always performs at least one attempt");
216 let page = PageInfo {
217 url: page_state.url,
218 title: page_state.title,
219 ready_state: page_state.ready_state,
220 target_id,
221 frame_id,
222 };
223 let full_roots = parse_accessibility_tree(&accessibility_raw);
224 let mut compact_accessibility =
225 crate::browser::dom::project_compact_accessibility_with_ranking(
226 &full_roots,
227 end_revision,
228 ranking,
229 );
230 let (mut text, locally_truncated) =
231 truncate_visible_text_with_status(&page_state.text, COMPACT_TEXT_MAX_BYTES);
232 let text_truncated = locally_truncated || page_state.boundaries.text_truncated;
233 if page_state.boundaries.text_truncated && !text.ends_with(TEXT_TRUNCATION_MARKER) {
234 let content_limit = COMPACT_TEXT_MAX_BYTES.saturating_sub(TEXT_TRUNCATION_MARKER.len());
235 while text.len() > content_limit {
236 text.pop();
237 }
238 text.push_str(TEXT_TRUNCATION_MARKER);
239 }
240 let mut incomplete = Vec::new();
241 if text_truncated {
242 incomplete.push(ObservationIncompleteReason::VisibleText);
243 }
244 if compact_accessibility.nodes_truncated {
245 incomplete.push(ObservationIncompleteReason::AccessibilityNode);
246 }
247 if compact_accessibility.labels_truncated {
248 incomplete.push(ObservationIncompleteReason::AccessibilityLabel);
249 }
250 if compact_accessibility.controls_truncated {
251 incomplete.push(ObservationIncompleteReason::Control);
252 }
253 if page_state.boundaries.child_frames > 0 {
254 incomplete.push(ObservationIncompleteReason::FrameBoundary);
255 }
256 if page_state.boundaries.canvases > 0 {
257 incomplete.push(ObservationIncompleteReason::Canvas);
258 }
259 if page_state.boundaries.truncated {
260 incomplete.push(ObservationIncompleteReason::BoundaryScan);
261 }
262 if !consistent {
263 incomplete.push(ObservationIncompleteReason::MutationRace);
264 }
265 let (shadow_paths, pierced_hosts) = if page_state.boundaries.shadow_roots > 0 {
267 match self
268 .cdp
269 .get_flattened_document(crate::browser::dom::MAX_SHADOW_DEPTH as i64)
270 .await
271 {
272 Ok(flattened) => {
273 let paths = crate::browser::dom::build_shadow_host_paths(&flattened);
274 let hosts = crate::browser::dom::count_pierced_shadow_hosts(&paths);
275 (paths, hosts)
276 }
277 Err(_) => (HashMap::new(), 0),
278 }
279 } else {
280 (HashMap::new(), 0)
281 };
282
283 if page_state.boundaries.shadow_roots > 0
285 && pierced_hosts < page_state.boundaries.shadow_roots
286 {
287 incomplete.push(ObservationIncompleteReason::ShadowBoundary);
288 }
289
290 if !shadow_paths.is_empty() {
292 for control in compact_accessibility.interactive.iter_mut() {
293 if let Some(path) = shadow_paths.get(&control.backend_dom_node_id) {
294 control.shadow_host_path = Some(path.clone());
295 }
296 }
297 }
298
299 if include_form_values {
301 self.read_form_field_values(&mut compact_accessibility.interactive)
302 .await?;
303 }
304
305 let interactive_len = compact_accessibility.interactive.len();
306 let accessibility = CompactAccessibilitySnapshot {
307 page: page.clone(),
308 revision: end_revision,
309 roots: compact_accessibility.roots,
310 interactive: compact_accessibility.interactive,
311 truncated: compact_accessibility.truncated,
312 omitted_count: compact_accessibility.omitted_count,
313 ranking_applied: compact_accessibility.ranking_applied,
314 completeness: Some(ObservationCompleteness::compute(
315 compact_accessibility.interactive_discovered,
316 interactive_len,
317 page_state.boundaries.shadow_roots,
318 pierced_hosts,
319 page_state.boundaries.canvases,
320 page_state.boundaries.child_frames,
321 !consistent,
322 )),
323 };
324 let context = CompactPageContext {
325 page,
326 text,
327 accessibility,
328 consistency: ObservationConsistency {
329 consistent,
330 attempts,
331 start_revision,
332 end_revision,
333 start_mutation_revision: start_state.mutation_revision,
334 end_mutation_revision: page_state.mutation_revision,
335 },
336 boundaries: page_state.boundaries,
337 incomplete,
338 };
339 if consistent && self.page_revision.load(Ordering::Relaxed) == end_revision {
340 *self.observation_cache.lock().await = Some(CachedObservation {
341 revision: end_revision,
342 context: context.clone(),
343 });
344 }
345 Ok(context)
346 }
347
348 async fn read_form_field_values(
351 &self,
352 controls: &mut [CompactInteractiveElement],
353 ) -> BrowserResult<()> {
354 use crate::browser::dom::{
355 FORM_VALUE_MAX_BYTES, FORM_VALUE_MAX_FIELDS, SELECT_OPTION_MAX_BYTES, truncate_utf8,
356 };
357
358 self.policy.require(PolicyCapability::ReadFormValues)?;
359 let allow_sensitive = self.policy.allow_sensitive_form_values();
360
361 const FORM_ROLES: &[&str] = &[
362 "textbox",
363 "searchbox",
364 "combobox",
365 "spinbutton",
366 "listbox",
367 "checkbox",
368 "radio",
369 "switch",
370 "slider",
371 ];
372
373 let mut candidates: Vec<&mut CompactInteractiveElement> = controls
375 .iter_mut()
376 .filter(|c| {
377 FORM_ROLES.iter().any(|r| c.role.eq_ignore_ascii_case(r))
378 && c.backend_dom_node_id > 0
379 })
380 .take(FORM_VALUE_MAX_FIELDS)
381 .collect();
382
383 if candidates.is_empty() {
384 return Ok(());
385 }
386
387 let expression = r#"function() {
389 const el = this;
390 const result = { empty: true };
391 const tag = (el.tagName || '').toLowerCase();
392 if (tag === 'input') {
393 const type = (el.type || 'text').toLowerCase();
394 if (type === 'checkbox' || type === 'radio') {
395 result.checked = el.checked;
396 result.value = el.value;
397 } else {
398 result.value = el.value;
399 }
400 } else if (tag === 'select') {
401 const opt = el.options[el.selectedIndex];
402 result.selectedOption = opt ? (opt.label || opt.text || opt.value) : '';
403 result.value = el.value;
404 } else if (tag === 'textarea') {
405 result.value = el.value;
406 } else {
407 result.value = el.value || el.textContent || '';
408 }
409 result.empty = !result.value && !result.selectedOption && !result.checked;
410 result.readOnly = !!el.readOnly;
411 result.required = !!el.required;
412 result.autocomplete = el.getAttribute('autocomplete') || '';
413 result.inputType = (el.type || '').toLowerCase();
414 return JSON.stringify(result);
415 }"#;
416
417 for control in candidates.iter_mut() {
418 let resolved = match self
419 .cdp
420 .send(
421 "DOM.resolveNode",
422 Some(serde_json::json!({
423 "backendNodeId": control.backend_dom_node_id,
424 })),
425 )
426 .await
427 {
428 Ok(resolved) => resolved,
429 Err(_) => continue,
430 };
431
432 let Some(object_id) = resolved["object"]["objectId"].as_str() else {
433 continue;
434 };
435 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id.to_string());
436
437 let raw_result = self
438 .cdp
439 .send(
440 "Runtime.callFunctionOn",
441 Some(serde_json::json!({
442 "objectId": &remote.object_id,
443 "functionDeclaration": expression,
444 "returnByValue": true,
445 "awaitPromise": false,
446 })),
447 )
448 .await;
449 let raw = match raw_result {
450 Ok(raw) => raw,
451 Err(_) => continue,
452 };
453
454 let value_str = raw["result"]["value"].as_str().unwrap_or("{}");
455 let parsed: Value = match serde_json::from_str(value_str) {
456 Ok(v) => v,
457 Err(_) => continue,
458 };
459
460 let input_type = parsed["inputType"]
461 .as_str()
462 .map(String::from)
463 .or_else(|| control.input_type.clone());
464
465 let is_password = input_type.as_deref() == Some("password");
466 let is_sensitive_autocomplete = parsed["autocomplete"]
467 .as_str()
468 .map(|ac| ac.starts_with("cc-") || ac == "current-password" || ac == "new-password")
469 .unwrap_or(false);
470
471 if let Some(val) = parsed["value"].as_str() {
472 if (is_password || is_sensitive_autocomplete) && !allow_sensitive {
473 control.value = Some("<redacted>".to_string());
474 } else {
475 let (truncated, _) = truncate_utf8(val, FORM_VALUE_MAX_BYTES);
476 control.value = Some(truncated.to_string());
477 }
478 }
479
480 if let Some(checked) = parsed["checked"].as_bool() {
481 control.checked = Some(checked);
482 }
483
484 if let Some(opt) = parsed["selectedOption"].as_str() {
485 let (truncated, _) = truncate_utf8(opt, SELECT_OPTION_MAX_BYTES);
486 control.selected_option = Some(truncated.to_string());
487 }
488
489 control.empty = parsed["empty"].as_bool().unwrap_or(true);
490 control.read_only = parsed["readOnly"].as_bool().unwrap_or(false);
491 control.required = parsed["required"].as_bool().unwrap_or(false);
492
493 if let Some(it) = input_type {
494 control.input_type = Some(it);
495 }
496 }
497
498 Ok(())
499 }
500
501 async fn compact_page_state(&self, context_id: i64) -> BrowserResult<EvaluatedPageState> {
502 let raw = self
503 .cdp
504 .evaluate_in_context(COMPACT_PAGE_STATE_EXPRESSION, Some(context_id))
505 .await?;
506 Ok(serde_json::from_value(runtime_value(&raw)?)?)
507 }
508
509 async fn observation_context_id(&self, target_id: &str, frame_id: &str) -> BrowserResult<i64> {
510 let session_id = self.cdp.current_session_id();
511 {
512 let context = self.observation_context.lock().await;
513 if let Some(cached) = context.as_ref()
514 && cached.target_id == target_id
515 && cached.session_id == session_id
516 && cached.frame_id == frame_id
517 {
518 return Ok(cached.context_id);
519 }
520 }
521
522 let world = self
523 .cdp
524 .send(
525 "Page.createIsolatedWorld",
526 Some(serde_json::json!({"frameId": frame_id, "worldName": "glass-observation"})),
527 )
528 .await?;
529 let context_id = world["executionContextId"]
530 .as_i64()
531 .ok_or("Page.createIsolatedWorld returned no executionContextId")?;
532 *self.observation_context.lock().await = Some(CachedObservationContext {
533 target_id: target_id.to_string(),
534 session_id,
535 frame_id: frame_id.to_string(),
536 context_id,
537 });
538 Ok(context_id)
539 }
540
541 async fn discard_observation_context(&self, target_id: &str, frame_id: &str, context_id: i64) {
542 let mut context = self.observation_context.lock().await;
543 if context.as_ref().is_some_and(|cached| {
544 cached.target_id == target_id
545 && cached.frame_id == frame_id
546 && cached.context_id == context_id
547 }) {
548 context.take();
549 }
550 }
551}
552
553fn is_stale_observation_context(error: &(dyn std::error::Error + 'static)) -> bool {
554 error
555 .downcast_ref::<crate::browser::cdp::CdpError>()
556 .is_some_and(|error| {
557 error.message.contains("Cannot find context")
558 || error.message.contains("Execution context was destroyed")
559 })
560}