playwright update

This commit is contained in:
Simon
2026-09-07 15:59:03 +00:00
parent 86c8faca44
commit 16219e1ddf
2 changed files with 36 additions and 27 deletions

View File

@@ -45,7 +45,7 @@ aes-gcm = "0.10"
cbc = { version = "0.1", features = ["alloc"] } cbc = { version = "0.1", features = ["alloc"] }
hex = "0.4" hex = "0.4"
chromiumoxide = { version = "0.7", features = ["tokio-runtime"] } chromiumoxide = { version = "0.7", features = ["tokio-runtime"] }
playwright = "0.0.20" playwright-rs = "0.7"
[lints.rust] [lints.rust]
warnings = "warn" warnings = "warn"

View File

@@ -1,5 +1,4 @@
use playwright::Playwright; use playwright_rs::{LaunchOptions, Page, Playwright};
use playwright::api::Page;
use std::path::Path; use std::path::Path;
use std::time::Duration; use std::time::Duration;
use tokio::time::{sleep, timeout}; use tokio::time::{sleep, timeout};
@@ -52,24 +51,29 @@ pub async fn wait_for_attribute(
attribute: &str, attribute: &str,
wait_timeout: Duration, wait_timeout: Duration,
) -> Option<String> { ) -> Option<String> {
// _pw must be kept alive — dropping it kills the driver process. // _browser is kept alive (Arc inside Playwright) — the page borrows from it.
let (_pw, page) = open_page(url).await?; // 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 xpath_js = serde_json::to_string(xpath).unwrap_or_default();
let attr_js = serde_json::to_string(attribute).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!( let js = format!(
r#"() => {{ r#"(() => {{
var r = document.evaluate({xpath_js}, document, null, var r = document.evaluate({xpath_js}, document, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null); XPathResult.FIRST_ORDERED_NODE_TYPE, null);
var el = r.singleNodeValue; var el = r.singleNodeValue;
if (!el) return ''; if (!el) return '';
return el.getAttribute({attr_js}) || el[{attr_js}] || ''; return el.getAttribute({attr_js}) || el[{attr_js}] || '';
}}"# }})()"#
); );
let result = timeout(wait_timeout, async { let result = timeout(wait_timeout, async {
loop { loop {
match page.evaluate::<(), String>(&js, ()).await { match page.evaluate_value(&js).await {
Ok(s) if !s.is_empty() => return Some(s), Ok(s) if !s.is_empty() => return Some(s),
Ok(_) => {} Ok(_) => {}
Err(e) => eprintln!("[playwright] evaluate error: {e}"), Err(e) => eprintln!("[playwright] evaluate error: {e}"),
@@ -86,43 +90,48 @@ pub async fn wait_for_attribute(
// ── internals ───────────────────────────────────────────────────────────────── // ── internals ─────────────────────────────────────────────────────────────────
async fn open_page(url: &str) -> Option<(Playwright, Page)> { /// Launches Playwright (Node server + bundled Chromium), opens a page on
let pw = Playwright::initialize() /// `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 .await
.map_err(|e| eprintln!("[playwright] init error: {e}")) .map_err(|e| eprintln!("[playwright] init error: {e}"))
.ok()?; .ok()?;
let args: Vec<String> = CHROME_ARGS.iter().map(|s| s.to_string()).collect(); let args: Vec<String> = CHROME_ARGS.iter().map(|s| s.to_string()).collect();
let browser = pw let options = LaunchOptions::new()
.chromium()
.launcher()
.executable(Path::new("/usr/bin/google-chrome"))
.headless(true) .headless(true)
.args(&args) .chromium_sandbox(false)
.launch() .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 .await
.map_err(|e| eprintln!("[playwright] launch error: {e}")) .map_err(|e| eprintln!("[playwright] launch error: {e}"))
.ok()?; .ok()?;
let context = browser let page = browser
.context_builder()
.build()
.await
.map_err(|e| eprintln!("[playwright] context error: {e}"))
.ok()?;
let page = context
.new_page() .new_page()
.await .await
.map_err(|e| eprintln!("[playwright] new_page error: {e}")) .map_err(|e| eprintln!("[playwright] new_page error: {e}"))
.ok()?; .ok()?;
page.goto_builder(url) page.goto(url, None)
.goto()
.await .await
.map_err(|e| eprintln!("[playwright] goto error: {e}")) .map_err(|e| eprintln!("[playwright] goto error: {e}"))
.ok()?; .ok()?;
Some((pw, page)) Some((page, browser, playwright))
} }