Skip to main content

argus_fetcher/
js_render.rs

1#[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}