diff --git a/Cargo.toml b/Cargo.toml index fdd9539..f19d7bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ aes-gcm = "0.10" cbc = { version = "0.1", features = ["alloc"] } hex = "0.4" chromiumoxide = { version = "0.7", features = ["tokio-runtime"] } -playwright = "0.0.20" +playwright-rs = "0.7" [lints.rust] warnings = "warn" diff --git a/src/util/playwright.rs b/src/util/playwright.rs index e961b9e..043ec6b 100644 --- a/src/util/playwright.rs +++ b/src/util/playwright.rs @@ -1,5 +1,4 @@ -use playwright::Playwright; -use playwright::api::Page; +use playwright_rs::{LaunchOptions, Page, Playwright}; use std::path::Path; use std::time::Duration; use tokio::time::{sleep, timeout}; @@ -52,24 +51,29 @@ pub async fn wait_for_attribute( attribute: &str, wait_timeout: Duration, ) -> Option { - // _pw must be kept alive — dropping it kills the driver process. - let (_pw, page) = open_page(url).await?; + // _browser is kept alive (Arc inside Playwright) — the page borrows from it. + // The Page holds its own server reference, so dropping our local Playwright + // handle does not shut the driver down. + let (page, _browser, _playwright) = open_page(url).await?; let xpath_js = serde_json::to_string(xpath).unwrap_or_default(); let attr_js = serde_json::to_string(attribute).unwrap_or_default(); + // playwright-rs's evaluate_value takes a single JS expression, so wrap the + // multi-statement lookup in an IIFE. The return value is auto-converted to + // a String by the Playwright protocol layer. let js = format!( - r#"() => {{ + r#"(() => {{ var r = document.evaluate({xpath_js}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var el = r.singleNodeValue; if (!el) return ''; return el.getAttribute({attr_js}) || el[{attr_js}] || ''; - }}"# + }})()"# ); let result = timeout(wait_timeout, async { loop { - match page.evaluate::<(), String>(&js, ()).await { + match page.evaluate_value(&js).await { Ok(s) if !s.is_empty() => return Some(s), Ok(_) => {} Err(e) => eprintln!("[playwright] evaluate error: {e}"), @@ -86,43 +90,48 @@ pub async fn wait_for_attribute( // ── internals ───────────────────────────────────────────────────────────────── -async fn open_page(url: &str) -> Option<(Playwright, Page)> { - let pw = Playwright::initialize() +/// Launches Playwright (Node server + bundled Chromium), opens a page on +/// `url`, and returns a handle. Caller is responsible for keeping the +/// returned `Playwright` and `Browser` handles alive while the page is in +/// use — dropping the browser closes the page and driver. +async fn open_page( + url: &str, +) -> Option<(Page, playwright_rs::Browser, Playwright)> { + let playwright = Playwright::launch() .await .map_err(|e| eprintln!("[playwright] init error: {e}")) .ok()?; let args: Vec = CHROME_ARGS.iter().map(|s| s.to_string()).collect(); - let browser = pw - .chromium() - .launcher() - .executable(Path::new("/usr/bin/google-chrome")) + let options = LaunchOptions::new() .headless(true) - .args(&args) - .launch() + .chromium_sandbox(false) + .args(args) + .executable_path( + Path::new("/usr/bin/google-chrome") + .to_str() + .unwrap_or("/usr/bin/google-chrome") + .to_string(), + ); + + let browser = playwright + .chromium() + .launch_with_options(options) .await .map_err(|e| eprintln!("[playwright] launch error: {e}")) .ok()?; - let context = browser - .context_builder() - .build() - .await - .map_err(|e| eprintln!("[playwright] context error: {e}")) - .ok()?; - - let page = context + let page = browser .new_page() .await .map_err(|e| eprintln!("[playwright] new_page error: {e}")) .ok()?; - page.goto_builder(url) - .goto() + page.goto(url, None) .await .map_err(|e| eprintln!("[playwright] goto error: {e}")) .ok()?; - Some((pw, page)) + Some((page, browser, playwright)) }