allwright_surface_web/
frames.rs1use super::*;
2
3fn frame_expression(selector: &str) -> Result<String, String> {
4 let segments = selector_segments_literal("frame", selector)?;
5 let query = selector_chain_query_all_js();
6 Ok(format!(
7 r#"(() => {{
8 const selectorSegments = {segments};
9 const elements = {query};
10 if (elements.length !== 1) throw new Error('Frame requires exactly one element; found ' + elements.length);
11 const element = elements[0];
12 if (!['iframe', 'frame'].includes(element.localName)) throw new Error('Frame locator must resolve to an iframe');
13 if (!element.isConnected || !element.contentWindow) throw new Error('Frame is detached or not ready');
14 const initialDocument = element.contentDocument;
15 const src = (element.getAttribute('src') || '').trim();
16 if (initialDocument && initialDocument.URL === 'about:blank' &&
17 (element.hasAttribute('srcdoc') || (src && !src.startsWith('javascript:') && new URL(src, document.baseURI).href !== 'about:blank'))) {{
18 throw new Error('Frame initial navigation is pending');
19 }}
20 return element.contentWindow;
21 }})()"#
22 ))
23}
24
25const READY: &str = r#"new Promise(resolve => {
28 if (document.readyState !== 'complete') { resolve(false); return; }
29 let quiet;
30 const finish = value => { clearTimeout(quiet); clearTimeout(limit); observer.disconnect(); resolve(value); };
31 const arm = () => { clearTimeout(quiet); quiet = setTimeout(() => finish(document.readyState === 'complete'), 200); };
32 const observer = new MutationObserver(arm);
33 const limit = setTimeout(() => finish(false), 500);
34 observer.observe(document, {subtree:true, childList:true, attributes:true, characterData:true});
35 arm();
36})"#;
37
38fn frame_context(response: &Value) -> Result<String, String> {
39 if response
40 .pointer("/result/result/type")
41 .and_then(Value::as_str)
42 != Some("window")
43 {
44 return Err(format!(
45 "cannot resolve frame: {}",
46 response
47 .pointer("/result/exceptionDetails")
48 .unwrap_or(response)
49 ));
50 }
51 required_string(response, "/result/result/value/context")
52}
53fn ready(response: &Value) -> Result<(), String> {
54 if response
55 .pointer("/result/result/value")
56 .and_then(Value::as_bool)
57 == Some(true)
58 {
59 Ok(())
60 } else {
61 Err("frame document is still loading or changing".to_string())
62 }
63}
64
65pub async fn resolve_frame(
66 browser: &BrowserSessionHandle,
67 page: &PageSessionHandle,
68 selector: &str,
69) -> Result<PageInfo, String> {
70 let expression = frame_expression(selector)?;
71 let params = |context: &str, expression: &str| json!({"expression":expression,"target":{"context":context},"awaitPromise":true});
72 let page_session = match (browser, page) {
73 (
74 BrowserSessionHandle::Chromium { cdp_websocket_url },
75 PageSessionHandle::Chromium {
76 target_id,
77 browsing_context_id,
78 mapper_target_id,
79 },
80 ) => {
81 let (mut cdp, mapper, parent) = chromium_bidi_session(
82 cdp_websocket_url,
83 mapper_target_id.as_deref(),
84 Some(browsing_context_id.as_deref().unwrap_or(target_id)),
85 )
86 .await?;
87 let response = cdp.send_bidi_command(&mapper.mapper_session_id, &json!({"id":7100,"method":"script.evaluate","params":params(&parent, &expression)})).await?;
88 let context = frame_context(&response)?;
89 ready(
90 &cdp.send_bidi_command(
91 &mapper.mapper_session_id,
92 &json!({"id":7101,"method":"script.evaluate","params":params(&context, READY)}),
93 )
94 .await?,
95 )?;
96 let current = cdp.send_bidi_command(&mapper.mapper_session_id, &json!({"id":7102,"method":"script.evaluate","params":params(&parent, &expression)})).await?;
97 if frame_context(¤t)? != context {
98 return Err("frame was replaced while waiting".into());
99 }
100 PageSessionHandle::Chromium {
101 target_id: context.clone(),
102 browsing_context_id: Some(context),
103 mapper_target_id: Some(mapper.mapper_target_id),
104 }
105 }
106 (
107 BrowserSessionHandle::Firefox { connection_id, .. },
108 PageSessionHandle::Firefox {
109 browsing_context_id,
110 },
111 ) => {
112 let mut sessions = firefox_session_guard(connection_id).await?;
113 let bidi = &mut sessions.get_mut(connection_id).unwrap().connection;
114 let context = frame_context(
115 &bidi
116 .send_command("script.evaluate", params(browsing_context_id, &expression))
117 .await?,
118 )?;
119 bidi.frame_contexts.insert(context.clone());
120 ready(
121 &bidi
122 .send_command("script.evaluate", params(&context, READY))
123 .await?,
124 )?;
125 let current = bidi
126 .send_command("script.evaluate", params(browsing_context_id, &expression))
127 .await?;
128 if frame_context(¤t)? != context {
129 return Err("frame was replaced while waiting".into());
130 }
131 PageSessionHandle::Firefox {
132 browsing_context_id: context,
133 }
134 }
135 _ => return Err("frame browser and page backend mismatch".into()),
136 };
137 Ok(PageInfo {
138 note: "resolved stable frame via WebDriver BiDi".into(),
139 page_session,
140 })
141}
142
143pub(super) fn tree_contains_context(tree: &Value, context: &str) -> bool {
145 fn contains(nodes: &[Value], context: &str) -> bool {
146 nodes.iter().any(|node| {
147 node.get("context").and_then(Value::as_str) == Some(context)
148 || node
149 .get("children")
150 .and_then(Value::as_array)
151 .is_some_and(|children| contains(children, context))
152 })
153 }
154 tree.pointer("/result/contexts")
155 .and_then(Value::as_array)
156 .is_some_and(|nodes| contains(nodes, context))
157}