use std::time::Duration;
use tracing::{debug, info, instrument};
use viewpoint_cdp::protocol::page::SetDocumentContentParams;
use viewpoint_cdp::protocol::runtime::EvaluateParams;
use crate::error::PageError;
use crate::wait::DocumentLoadState;
use super::Page;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScriptType {
#[default]
Script,
Module,
}
#[derive(Debug)]
pub struct ScriptTagBuilder<'a> {
page: &'a Page,
url: Option<String>,
content: Option<String>,
script_type: ScriptType,
}
impl<'a> ScriptTagBuilder<'a> {
pub(crate) fn new(page: &'a Page) -> Self {
Self {
page,
url: None,
content: None,
script_type: ScriptType::default(),
}
}
#[must_use]
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[must_use]
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
#[must_use]
pub fn script_type(mut self, script_type: ScriptType) -> Self {
self.script_type = script_type;
self
}
#[instrument(level = "debug", skip(self), fields(has_url = self.url.is_some(), has_content = self.content.is_some()))]
pub async fn inject(self) -> Result<(), PageError> {
if self.page.is_closed() {
return Err(PageError::Closed);
}
let script_js = if let Some(url) = self.url {
format!(
r"
new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = '{}';
script.setAttribute('type', '{}');
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}})
",
url.replace('\'', "\\'"),
if self.script_type == ScriptType::Module {
"module"
} else {
"text/javascript"
}
)
} else if let Some(content) = self.content {
format!(
r"
(() => {{
const script = document.createElement('script');
script.textContent = {};
{}
document.head.appendChild(script);
}})()
",
serde_json::to_string(&content).unwrap_or_default(),
if self.script_type == ScriptType::Module {
"script.type = 'module';"
} else {
""
}
)
} else {
return Err(PageError::EvaluationFailed(
"Either url or content must be provided".to_string(),
));
};
debug!("Injecting script tag");
self.page
.connection()
.send_command::<_, serde_json::Value>(
"Runtime.evaluate",
Some(EvaluateParams {
expression: script_js,
object_group: None,
include_command_line_api: None,
silent: Some(false),
context_id: None,
return_by_value: Some(true),
await_promise: Some(true),
}),
Some(self.page.session_id()),
)
.await?;
Ok(())
}
}
#[derive(Debug)]
pub struct StyleTagBuilder<'a> {
page: &'a Page,
url: Option<String>,
content: Option<String>,
}
impl<'a> StyleTagBuilder<'a> {
pub(crate) fn new(page: &'a Page) -> Self {
Self {
page,
url: None,
content: None,
}
}
#[must_use]
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[must_use]
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
#[instrument(level = "debug", skip(self), fields(has_url = self.url.is_some(), has_content = self.content.is_some()))]
pub async fn inject(self) -> Result<(), PageError> {
if self.page.is_closed() {
return Err(PageError::Closed);
}
let style_js = if let Some(url) = self.url {
format!(
r"
new Promise((resolve, reject) => {{
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '{}';
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
}})
",
url.replace('\'', "\\'")
)
} else if let Some(content) = self.content {
format!(
r"
(() => {{
const style = document.createElement('style');
style.textContent = {};
document.head.appendChild(style);
}})()
",
serde_json::to_string(&content).unwrap_or_default()
)
} else {
return Err(PageError::EvaluationFailed(
"Either url or content must be provided".to_string(),
));
};
debug!("Injecting style tag");
self.page
.connection()
.send_command::<_, serde_json::Value>(
"Runtime.evaluate",
Some(EvaluateParams {
expression: style_js,
object_group: None,
include_command_line_api: None,
silent: Some(false),
context_id: None,
return_by_value: Some(true),
await_promise: Some(true),
}),
Some(self.page.session_id()),
)
.await?;
Ok(())
}
}
#[derive(Debug)]
pub struct SetContentBuilder<'a> {
page: &'a Page,
html: String,
wait_until: DocumentLoadState,
timeout: Duration,
}
impl<'a> SetContentBuilder<'a> {
pub(crate) fn new(page: &'a Page, html: String) -> Self {
Self {
page,
html,
wait_until: DocumentLoadState::Load,
timeout: Duration::from_secs(30),
}
}
#[must_use]
pub fn wait_until(mut self, state: DocumentLoadState) -> Self {
self.wait_until = state;
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[instrument(level = "info", skip(self), fields(html_len = self.html.len(), wait_until = ?self.wait_until))]
pub async fn set(self) -> Result<(), PageError> {
if self.page.is_closed() {
return Err(PageError::Closed);
}
info!("Setting page content");
self.page
.connection()
.send_command::<_, serde_json::Value>(
"Page.setDocumentContent",
Some(SetDocumentContentParams {
frame_id: self.page.frame_id().to_string(),
html: self.html,
}),
Some(self.page.session_id()),
)
.await?;
if self.wait_until != DocumentLoadState::Commit {
tokio::time::sleep(Duration::from_millis(50)).await;
}
info!("Page content set");
Ok(())
}
}
impl Page {
#[instrument(level = "debug", skip(self))]
pub async fn content(&self) -> Result<String, PageError> {
if self.closed {
return Err(PageError::Closed);
}
let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
.connection
.send_command(
"Runtime.evaluate",
Some(EvaluateParams {
expression: r#"
(() => {
const doctype = document.doctype;
const doctypeString = doctype
? `<!DOCTYPE ${doctype.name}${doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ''}${doctype.systemId ? ` "${doctype.systemId}"` : ''}>`
: '';
return doctypeString + document.documentElement.outerHTML;
})()
"#.to_string(),
object_group: None,
include_command_line_api: None,
silent: Some(true),
context_id: None,
return_by_value: Some(true),
await_promise: Some(false),
}),
Some(&self.session_id),
)
.await?;
result
.result
.value
.and_then(|v| v.as_str().map(ToString::to_string))
.ok_or_else(|| PageError::EvaluationFailed("Failed to get content".to_string()))
}
pub fn set_content(&self, html: impl Into<String>) -> SetContentBuilder<'_> {
SetContentBuilder::new(self, html.into())
}
pub fn add_script_tag(&self) -> ScriptTagBuilder<'_> {
ScriptTagBuilder::new(self)
}
pub fn add_style_tag(&self) -> StyleTagBuilder<'_> {
StyleTagBuilder::new(self)
}
}