argus_fetcher/
js_render.rs1#[cfg(feature = "js-render")]
2use anyhow::{Context, Result};
3#[cfg(feature = "js-render")]
4use headless_chrome::{Browser, LaunchOptions};
5#[cfg(feature = "js-render")]
6use std::time::Duration;
7
8#[cfg(feature = "js-render")]
9#[derive(Clone)]
10pub struct JsRenderer {
11 headless: bool,
12 timeout: Duration,
13}
14
15#[cfg(feature = "js-render")]
16impl JsRenderer {
17 pub fn new() -> Self {
18 Self {
19 headless: true,
20 timeout: Duration::from_secs(30),
21 }
22 }
23
24 pub fn with_headless(mut self, headless: bool) -> Self {
25 self.headless = headless;
26 self
27 }
28
29 pub fn with_timeout(mut self, timeout: Duration) -> Self {
30 self.timeout = timeout;
31 self
32 }
33
34 pub async fn render(&self, url: &str) -> Result<String> {
35 let headless = self.headless;
36 let timeout = self.timeout;
37 let url = url.to_string();
38
39 tokio::task::spawn_blocking(move || {
40 let options = LaunchOptions {
41 headless,
42 ..Default::default()
43 };
44
45 let browser = Browser::new(options).context("failed to launch browser")?;
46 let tab = browser.new_tab().context("failed to create tab")?;
47
48 tab.navigate_to(&url).context("failed to navigate to URL")?;
49
50 tab.wait_for_element_with_custom_timeout("body", timeout)
51 .context("timeout waiting for body element")?;
52
53 std::thread::sleep(Duration::from_millis(500));
54
55 let content = tab.get_content().context("failed to get page content")?;
56
57 Ok(content)
58 })
59 .await
60 .context("render task panicked")?
61 }
62}
63
64#[cfg(feature = "js-render")]
65impl Default for JsRenderer {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71#[cfg(not(feature = "js-render"))]
72pub struct JsRenderer;
73
74#[cfg(not(feature = "js-render"))]
75impl JsRenderer {
76 pub fn new() -> Self {
77 Self
78 }
79
80 pub async fn render(&self, _url: &str) -> anyhow::Result<String> {
81 anyhow::bail!("JavaScript rendering not enabled. Compile with 'js-render' feature.")
82 }
83}
84
85#[cfg(not(feature = "js-render"))]
86impl Default for JsRenderer {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92#[cfg(all(test, feature = "js-render"))]
93mod tests {
94 use super::*;
95
96 #[tokio::test]
97 async fn render_simple_page() {
98 let renderer = JsRenderer::new();
99 let result = renderer.render("https://example.com").await;
100 assert!(result.is_ok());
101 let content = result.unwrap();
102 assert!(content.contains("Example Domain"));
103 }
104
105 #[tokio::test]
106 async fn render_with_custom_timeout() {
107 let renderer = JsRenderer::new().with_timeout(Duration::from_secs(10));
108 let result = renderer.render("https://example.com").await;
109 assert!(result.is_ok());
110 }
111}