viewpoint_core/page/content/
mod.rs1use std::time::Duration;
7
8use tracing::{debug, info, instrument};
9use viewpoint_cdp::protocol::page::SetDocumentContentParams;
10use viewpoint_cdp::protocol::runtime::EvaluateParams;
11
12use crate::error::PageError;
13use crate::wait::DocumentLoadState;
14
15use super::Page;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum ScriptType {
20 #[default]
22 Script,
23 Module,
25}
26
27#[derive(Debug)]
29pub struct ScriptTagBuilder<'a> {
30 page: &'a Page,
31 url: Option<String>,
32 content: Option<String>,
33 script_type: ScriptType,
34}
35
36impl<'a> ScriptTagBuilder<'a> {
37 pub(crate) fn new(page: &'a Page) -> Self {
39 Self {
40 page,
41 url: None,
42 content: None,
43 script_type: ScriptType::default(),
44 }
45 }
46
47 #[must_use]
49 pub fn url(mut self, url: impl Into<String>) -> Self {
50 self.url = Some(url.into());
51 self
52 }
53
54 #[must_use]
56 pub fn content(mut self, content: impl Into<String>) -> Self {
57 self.content = Some(content.into());
58 self
59 }
60
61 #[must_use]
63 pub fn script_type(mut self, script_type: ScriptType) -> Self {
64 self.script_type = script_type;
65 self
66 }
67
68 #[instrument(level = "debug", skip(self), fields(has_url = self.url.is_some(), has_content = self.content.is_some()))]
74 pub async fn inject(self) -> Result<(), PageError> {
75 if self.page.is_closed() {
76 return Err(PageError::Closed);
77 }
78
79 let script_js = if let Some(url) = self.url {
80 format!(
81 r"
82 new Promise((resolve, reject) => {{
83 const script = document.createElement('script');
84 script.src = '{}';
85 script.setAttribute('type', '{}');
86 script.onload = resolve;
87 script.onerror = reject;
88 document.head.appendChild(script);
89 }})
90 ",
91 url.replace('\'', "\\'"),
92 if self.script_type == ScriptType::Module { "module" } else { "text/javascript" }
93 )
94 } else if let Some(content) = self.content {
95 format!(
96 r"
97 (() => {{
98 const script = document.createElement('script');
99 script.textContent = {};
100 {}
101 document.head.appendChild(script);
102 }})()
103 ",
104 serde_json::to_string(&content).unwrap_or_default(),
105 if self.script_type == ScriptType::Module { "script.type = 'module';" } else { "" }
106 )
107 } else {
108 return Err(PageError::EvaluationFailed(
109 "Either url or content must be provided".to_string(),
110 ));
111 };
112
113 debug!("Injecting script tag");
114
115 self.page
116 .connection()
117 .send_command::<_, serde_json::Value>(
118 "Runtime.evaluate",
119 Some(EvaluateParams {
120 expression: script_js,
121 object_group: None,
122 include_command_line_api: None,
123 silent: Some(false),
124 context_id: None,
125 return_by_value: Some(true),
126 await_promise: Some(true),
127 }),
128 Some(self.page.session_id()),
129 )
130 .await?;
131
132 Ok(())
133 }
134}
135
136#[derive(Debug)]
138pub struct StyleTagBuilder<'a> {
139 page: &'a Page,
140 url: Option<String>,
141 content: Option<String>,
142}
143
144impl<'a> StyleTagBuilder<'a> {
145 pub(crate) fn new(page: &'a Page) -> Self {
147 Self {
148 page,
149 url: None,
150 content: None,
151 }
152 }
153
154 #[must_use]
156 pub fn url(mut self, url: impl Into<String>) -> Self {
157 self.url = Some(url.into());
158 self
159 }
160
161 #[must_use]
163 pub fn content(mut self, content: impl Into<String>) -> Self {
164 self.content = Some(content.into());
165 self
166 }
167
168 #[instrument(level = "debug", skip(self), fields(has_url = self.url.is_some(), has_content = self.content.is_some()))]
174 pub async fn inject(self) -> Result<(), PageError> {
175 if self.page.is_closed() {
176 return Err(PageError::Closed);
177 }
178
179 let style_js = if let Some(url) = self.url {
180 format!(
181 r"
182 new Promise((resolve, reject) => {{
183 const link = document.createElement('link');
184 link.rel = 'stylesheet';
185 link.href = '{}';
186 link.onload = resolve;
187 link.onerror = reject;
188 document.head.appendChild(link);
189 }})
190 ",
191 url.replace('\'', "\\'")
192 )
193 } else if let Some(content) = self.content {
194 format!(
195 r"
196 (() => {{
197 const style = document.createElement('style');
198 style.textContent = {};
199 document.head.appendChild(style);
200 }})()
201 ",
202 serde_json::to_string(&content).unwrap_or_default()
203 )
204 } else {
205 return Err(PageError::EvaluationFailed(
206 "Either url or content must be provided".to_string(),
207 ));
208 };
209
210 debug!("Injecting style tag");
211
212 self.page
213 .connection()
214 .send_command::<_, serde_json::Value>(
215 "Runtime.evaluate",
216 Some(EvaluateParams {
217 expression: style_js,
218 object_group: None,
219 include_command_line_api: None,
220 silent: Some(false),
221 context_id: None,
222 return_by_value: Some(true),
223 await_promise: Some(true),
224 }),
225 Some(self.page.session_id()),
226 )
227 .await?;
228
229 Ok(())
230 }
231}
232
233#[derive(Debug)]
235pub struct SetContentBuilder<'a> {
236 page: &'a Page,
237 html: String,
238 wait_until: DocumentLoadState,
239 timeout: Duration,
240}
241
242impl<'a> SetContentBuilder<'a> {
243 pub(crate) fn new(page: &'a Page, html: String) -> Self {
245 Self {
246 page,
247 html,
248 wait_until: DocumentLoadState::Load,
249 timeout: Duration::from_secs(30),
250 }
251 }
252
253 #[must_use]
255 pub fn wait_until(mut self, state: DocumentLoadState) -> Self {
256 self.wait_until = state;
257 self
258 }
259
260 #[must_use]
262 pub fn timeout(mut self, timeout: Duration) -> Self {
263 self.timeout = timeout;
264 self
265 }
266
267 #[instrument(level = "info", skip(self), fields(html_len = self.html.len(), wait_until = ?self.wait_until))]
273 pub async fn set(self) -> Result<(), PageError> {
274 if self.page.is_closed() {
275 return Err(PageError::Closed);
276 }
277
278 info!("Setting page content");
279
280 self.page
282 .connection()
283 .send_command::<_, serde_json::Value>(
284 "Page.setDocumentContent",
285 Some(SetDocumentContentParams {
286 frame_id: self.page.frame_id().to_string(),
287 html: self.html,
288 }),
289 Some(self.page.session_id()),
290 )
291 .await?;
292
293 if self.wait_until != DocumentLoadState::Commit {
297 tokio::time::sleep(Duration::from_millis(50)).await;
299 }
300
301 info!("Page content set");
302 Ok(())
303 }
304}
305
306impl Page {
307 #[instrument(level = "debug", skip(self))]
323 pub async fn content(&self) -> Result<String, PageError> {
324 if self.closed {
325 return Err(PageError::Closed);
326 }
327
328 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
329 .connection
330 .send_command(
331 "Runtime.evaluate",
332 Some(EvaluateParams {
333 expression: r#"
334 (() => {
335 const doctype = document.doctype;
336 const doctypeString = doctype
337 ? `<!DOCTYPE ${doctype.name}${doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ''}${doctype.systemId ? ` "${doctype.systemId}"` : ''}>`
338 : '';
339 return doctypeString + document.documentElement.outerHTML;
340 })()
341 "#.to_string(),
342 object_group: None,
343 include_command_line_api: None,
344 silent: Some(true),
345 context_id: None,
346 return_by_value: Some(true),
347 await_promise: Some(false),
348 }),
349 Some(&self.session_id),
350 )
351 .await?;
352
353 result
354 .result
355 .value
356 .and_then(|v| v.as_str().map(ToString::to_string))
357 .ok_or_else(|| PageError::EvaluationFailed("Failed to get content".to_string()))
358 }
359
360 pub fn set_content(&self, html: impl Into<String>) -> SetContentBuilder<'_> {
382 SetContentBuilder::new(self, html.into())
383 }
384
385 pub fn add_script_tag(&self) -> ScriptTagBuilder<'_> {
408 ScriptTagBuilder::new(self)
409 }
410
411 pub fn add_style_tag(&self) -> StyleTagBuilder<'_> {
426 StyleTagBuilder::new(self)
427 }
428}