use futures_util::{SinkExt, StreamExt};
use playwright_rs::protocol::Playwright;
use playwright_rs::server::channel_owner::ChannelOwner;
use playwright_rs::server::connection::Connection;
use playwright_rs::server::playwright_server::PlaywrightServer;
use playwright_rs::server::transport::PipeTransport;
use serde_json::json;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::process::Command;
use tokio_tungstenite::accept_async;
use tokio_tungstenite::tungstenite::protocol::Message;
#[tokio::test]
async fn test_connection_lifecycle_with_real_server() {
crate::common::init_tracing();
let mut server = match PlaywrightServer::launch().await {
Ok(s) => s,
Err(e) => {
tracing::warn!("Skipping test: Could not launch Playwright server: {}", e);
tracing::warn!("This is expected if Node.js or Playwright driver is not available");
return;
}
};
let stdin = server.process.stdin.take().expect("Failed to get stdin");
let stdout = server.process.stdout.take().expect("Failed to get stdout");
let (transport, message_rx) =
playwright_rs::server::transport::PipeTransport::new(stdin, stdout);
let (sender, receiver) = transport.into_parts();
let connection = Arc::new(Connection::new(sender, receiver, message_rx));
let conn = Arc::clone(&connection);
let connection_handle = tokio::spawn(async move {
conn.run().await;
});
tokio::time::sleep(Duration::from_millis(100)).await;
connection_handle.abort();
server.shutdown().await.ok();
}
#[tokio::test]
async fn test_connection_detects_server_crash_on_send() {
crate::common::init_tracing();
let mut server = match PlaywrightServer::launch().await {
Ok(s) => s,
Err(e) => {
tracing::warn!("Skipping test: Could not launch Playwright server: {}", e);
return;
}
};
let stdin = server.process.stdin.take().expect("Failed to get stdin");
let stdout = server.process.stdout.take().expect("Failed to get stdout");
let (transport, message_rx) =
playwright_rs::server::transport::PipeTransport::new(stdin, stdout);
let (sender, receiver) = transport.into_parts();
let connection = Arc::new(Connection::new(sender, receiver, message_rx));
let conn = Arc::clone(&connection);
let _connection_handle = tokio::spawn(async move {
conn.run().await;
});
tokio::time::sleep(Duration::from_millis(50)).await;
server.kill().await.expect("Failed to kill server");
tokio::time::sleep(Duration::from_millis(30)).await;
let send_result = connection
.send_message(
"test@guid".to_string(),
"testMethod".to_string(),
serde_json::json!({}),
)
.await;
assert!(
send_result.is_err(),
"Expected error when sending to dead server"
);
match send_result.unwrap_err() {
playwright_rs::Error::TransportError(msg) => {
assert!(
msg.contains("Broken pipe") || msg.contains("Failed to write"),
"Expected broken pipe error, got: {}",
msg
);
}
e => panic!("Expected TransportError, got: {:?}", e),
}
}
#[tokio::test]
async fn test_concurrent_requests_with_server() {
crate::common::init_tracing();
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let chromium = playwright.chromium();
let browser = chromium.launch().await.expect("Failed to launch browser");
let context1_fut = browser.new_context();
let context2_fut = browser.new_context();
let (context1, context2) = tokio::join!(context1_fut, context2_fut);
let context1 = context1.expect("Failed to create context 1");
let context2 = context2.expect("Failed to create context 2");
let page1_fut = context1.new_page();
let page2_fut = context1.new_page();
let page3_fut = context2.new_page();
let (page1, page2, page3) = tokio::join!(page1_fut, page2_fut, page3_fut);
let page1 = page1.expect("Failed to create page 1");
let page2 = page2.expect("Failed to create page 2");
let page3 = page3.expect("Failed to create page 3");
assert_eq!(page1.url(), "about:blank");
assert_eq!(page2.url(), "about:blank");
assert_eq!(page3.url(), "about:blank");
let page1_close = page1.close();
let page2_close = page2.close();
let page3_close = page3.close();
let context1_close = context1.close();
let context2_close = context2.close();
let (r1, r2, r3, r4, r5) = tokio::join!(
page1_close,
page2_close,
page3_close,
context1_close,
context2_close
);
r1.expect("Failed to close page 1");
r2.expect("Failed to close page 2");
r3.expect("Failed to close page 3");
r4.expect("Failed to close context 1");
r5.expect("Failed to close context 2");
browser.close().await.expect("Failed to close browser");
}
#[tokio::test]
async fn test_error_response_from_server() {
crate::common::init_tracing();
let playwright = Playwright::launch()
.await
.expect("Failed to launch Playwright");
let chromium = playwright.chromium();
let browser = chromium.launch().await.expect("Failed to launch browser");
browser.close().await.expect("Failed to close browser");
let result = browser.close().await;
assert!(
result.is_err(),
"Expected error when closing already-closed browser"
);
}
#[tokio::test]
async fn test_connect_over_cdp_chromium_only() {
crate::common::init_tracing();
let playwright = match Playwright::launch().await {
Ok(p) => p,
Err(e) => {
tracing::warn!("Skipping test: Failed to launch Playwright: {}", e);
return;
}
};
let result = playwright
.firefox()
.connect_over_cdp("http://localhost:9222", None)
.await;
assert!(
result.is_err(),
"Firefox should not support connect_over_cdp"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("Chromium"),
"Error should mention Chromium: {}",
err
);
let result = playwright
.webkit()
.connect_over_cdp("http://localhost:9222", None)
.await;
assert!(
result.is_err(),
"WebKit should not support connect_over_cdp"
);
playwright.shutdown().await.ok();
}
async fn start_chrome_with_cdp(
package_path: &std::path::Path,
) -> Option<(tokio::process::Child, String)> {
let script = format!(
r#"
const {{ chromium }} = require('{}');
const {{ spawn }} = require('child_process');
const http = require('http');
const execPath = chromium.executablePath();
const child = spawn(execPath, [
'--headless',
'--remote-debugging-port=0',
'--no-sandbox',
'--disable-gpu',
'--use-mock-keychain',
'--no-first-run'
], {{ stdio: ['pipe', 'pipe', 'pipe'] }});
// Chrome outputs the DevTools URL to stderr
let stderr = '';
child.stderr.on('data', (data) => {{
stderr += data.toString();
// Look for the DevTools listening message
const match = stderr.match(/DevTools listening on (ws:\/\/[^\s]+)/);
if (match) {{
// Extract port from ws://127.0.0.1:PORT/devtools/browser/...
const portMatch = match[1].match(/:(\d+)\//);
if (portMatch) {{
console.log('http://127.0.0.1:' + portMatch[1]);
}}
}}
}});
// Keep running until stdin closes
process.stdin.resume();
process.stdin.on('close', () => {{
child.kill();
process.exit(0);
}});
// Also kill on timeout (safety)
setTimeout(() => {{
child.kill();
process.exit(1);
}}, 25000);
"#,
package_path.display()
);
let mut child = Command::new("node")
.arg("-e")
.arg(&script)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
let stdout = child.stdout.take()?;
let mut reader = tokio::io::BufReader::new(stdout).lines();
let endpoint = tokio::time::timeout(Duration::from_secs(15), async {
while let Ok(Some(line)) = reader.next_line().await {
if line.starts_with("http://") || line.starts_with("ws://") {
return Some(line);
}
}
None
})
.await
.ok()??;
Some((child, endpoint))
}
#[tokio::test]
async fn test_connect_over_cdp_real_chrome() {
crate::common::init_tracing();
let drivers_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("drivers");
let package_path = std::fs::read_dir(&drivers_dir)
.ok()
.and_then(|mut entries| entries.next())
.and_then(|e| e.ok())
.map(|e| e.path().join("package"));
let package_path = match package_path {
Some(p) if p.exists() => p,
_ => {
tracing::warn!("Skipping test: Playwright driver not found");
return;
}
};
let (mut chrome_process, cdp_endpoint) = match start_chrome_with_cdp(&package_path).await {
Some(result) => result,
None => {
tracing::warn!("Skipping test: Failed to start Chrome with CDP");
return;
}
};
tracing::info!("Chrome CDP endpoint: {}", cdp_endpoint);
let playwright = match Playwright::launch().await {
Ok(p) => p,
Err(e) => {
tracing::warn!("Skipping test: Failed to launch Playwright: {}", e);
let _ = chrome_process.kill().await;
return;
}
};
let browser = match playwright
.chromium()
.connect_over_cdp(&cdp_endpoint, None)
.await
{
Ok(b) => b,
Err(e) => {
tracing::error!("Failed to connect over CDP: {}", e);
let _ = playwright.shutdown().await;
let _ = chrome_process.kill().await;
panic!("connect_over_cdp failed: {:?}", e);
}
};
tracing::info!("Connected via CDP! Browser version: {}", browser.version());
assert!(browser.is_connected());
assert!(!browser.version().is_empty());
let page = browser.new_page().await.expect("Failed to create page");
page.goto("data:text/html,<h1>CDP Connection Works!</h1>", None)
.await
.expect("Failed to navigate");
let heading = page.locator("h1").await;
let text = heading.text_content().await.expect("Failed to get text");
assert_eq!(text, Some("CDP Connection Works!".to_string()));
tracing::info!("CDP connection test passed!");
browser.close().await.ok();
playwright.shutdown().await.ok();
let _ = chrome_process.kill().await;
}
#[tokio::test]
async fn test_browser_type_connect() {
eprintln!("Test starting");
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
eprintln!("Remote Mock server bound");
let addr = listener.local_addr().unwrap();
let url = format!("ws://127.0.0.1:{}", addr.port());
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut ws_stream = accept_async(stream).await.unwrap();
let browser_guid = "browser@remote";
let types = vec!["chromium", "firefox", "webkit"];
for t in types {
let create_type = json!({
"guid": "", "method": "__create__",
"params": {
"type": "BrowserType",
"guid": format!("browserType@{}", t),
"initializer": {
"name": t,
"executablePath": "/bin/browser"
}
}
});
ws_stream
.send(Message::Text(create_type.to_string().into()))
.await
.unwrap();
}
let create_browser = json!({
"guid": "browserType@chromium",
"method": "__create__",
"params": {
"type": "Browser",
"guid": browser_guid,
"initializer": {
"name": "chromium",
"executablePath": "/bin/chromium",
"version": "1.0"
}
}
});
ws_stream
.send(Message::Text(create_browser.to_string().into()))
.await
.unwrap();
let create_playwright = json!({
"guid": "", "method": "__create__",
"params": {
"type": "Playwright",
"guid": "playwright",
"initializer": {
"chromium": { "guid": "browserType@chromium" },
"firefox": { "guid": "browserType@firefox" },
"webkit": { "guid": "browserType@webkit" },
"preLaunchedBrowser": { "guid": browser_guid }
}
}
});
ws_stream
.send(Message::Text(create_playwright.to_string().into()))
.await
.unwrap();
while let Some(msg) = ws_stream.next().await {
match msg {
Ok(Message::Text(text)) => {
if let Ok(request) = serde_json::from_str::<serde_json::Value>(&text) {
if request.get("method").and_then(|m| m.as_str()) == Some("initialize") {
let id = request.get("id").and_then(|i| i.as_u64()).unwrap_or(0);
let response = json!({
"id": id,
"result": {
"playwright": {
"guid": "playwright"
}
}
});
ws_stream
.send(Message::Text(response.to_string().into()))
.await
.unwrap();
}
}
}
Ok(Message::Close(_)) => break,
Ok(_) => {}
Err(_) => break,
}
}
});
let (client_conn, mut server_conn) = tokio::io::duplex(65536);
tokio::spawn(async move {
async fn send_framed(stream: &mut tokio::io::DuplexStream, msg: serde_json::Value) {
let bytes = serde_json::to_vec(&msg).unwrap();
let len = bytes.len() as u32;
stream.write_all(&len.to_le_bytes()).await.unwrap();
stream.write_all(&bytes).await.unwrap();
}
let types = vec!["chromium", "firefox", "webkit"];
for t in types {
let create_type = json!({
"guid": "",
"method": "__create__",
"params": {
"type": "BrowserType",
"guid": format!("browserType@{}", t),
"initializer": {
"name": t,
"executablePath": "/bin/browser"
}
}
});
send_framed(&mut server_conn, create_type).await;
}
let create_playwright = json!({
"guid": "",
"method": "__create__",
"params": {
"type": "Playwright",
"guid": "playwright",
"initializer": {
"chromium": { "guid": "browserType@chromium" },
"firefox": { "guid": "browserType@firefox" },
"webkit": { "guid": "browserType@webkit" }
}
}
});
send_framed(&mut server_conn, create_playwright).await;
let mut len_buf = [0u8; 4];
server_conn.read_exact(&mut len_buf).await.unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
let mut msg_buf = vec![0u8; len];
server_conn.read_exact(&mut msg_buf).await.unwrap();
let msg: serde_json::Value = serde_json::from_slice(&msg_buf).unwrap();
if let Some(id) = msg["id"].as_i64() {
let response = json!({
"id": id,
"result": {
"playwright": {
"guid": "playwright" }
}
});
send_framed(&mut server_conn, response).await;
}
let mut buf = vec![0u8; 1024];
loop {
if server_conn.read(&mut buf).await.unwrap() == 0 {
break;
}
}
});
let (client_r, client_w) = tokio::io::split(client_conn);
let (transport, message_rx) = PipeTransport::new(client_w, client_r);
let (sender, receiver) = transport.into_parts();
let connection = Arc::new(Connection::new(sender, receiver, message_rx));
let conn_clone = connection.clone();
tokio::spawn(async move {
conn_clone.run().await;
});
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
eprintln!("Initializing local playwright");
let playwright_obj = connection
.initialize_playwright()
.await
.expect("Local init failed");
let playwright = playwright_obj
.as_any()
.downcast_ref::<Playwright>()
.unwrap();
eprintln!("Local playwright initialized");
eprintln!("Connecting to remote: {}", url);
let browser = playwright
.chromium()
.connect(&url, None)
.await
.expect("Connect failed");
eprintln!("Connected!");
assert_eq!(browser.guid(), "browser@remote");
}
async fn start_browser_server(
package_path: &std::path::Path,
) -> Option<(tokio::process::Child, String)> {
let script = format!(
r#"
const {{ chromium }} = require('{}');
(async () => {{
const server = await chromium.launchServer({{ headless: true }});
console.log(server.wsEndpoint());
// Keep running until stdin closes (parent process exits)
process.stdin.resume();
process.stdin.on('close', async () => {{
await server.close();
process.exit(0);
}});
}})().catch(err => {{
console.error(err);
process.exit(1);
}});
"#,
package_path.display()
);
let mut child = Command::new("node")
.arg("-e")
.arg(&script)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
let stdout = child.stdout.take()?;
let mut reader = BufReader::new(stdout).lines();
let ws_endpoint = tokio::time::timeout(Duration::from_secs(30), async {
if let Ok(Some(line)) = reader.next_line().await
&& line.starts_with("ws://")
{
return Some(line);
}
None
})
.await
.ok()??;
Some((child, ws_endpoint))
}
#[tokio::test]
async fn test_connect_to_real_server() {
crate::common::init_tracing();
let drivers_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("drivers");
let package_path = std::fs::read_dir(&drivers_dir)
.ok()
.and_then(|mut entries| entries.next())
.and_then(|e| e.ok())
.map(|e| e.path().join("package"));
let package_path = match package_path {
Some(p) if p.exists() => p,
_ => {
tracing::warn!(
"Skipping test: Playwright driver not found in {:?}",
drivers_dir
);
return;
}
};
tracing::info!("Starting browser server");
let (mut server_process, ws_endpoint) = match start_browser_server(&package_path).await {
Some(result) => result,
None => {
tracing::warn!("Skipping test: Failed to start browser server");
return;
}
};
tracing::info!("Browser server ready at {}", ws_endpoint);
let playwright = match Playwright::launch().await {
Ok(p) => p,
Err(e) => {
tracing::warn!("Skipping test: Failed to launch local Playwright: {}", e);
let _ = server_process.kill().await;
return;
}
};
tracing::info!("Connecting to remote server at {}", ws_endpoint);
let browser = match playwright.chromium().connect(&ws_endpoint, None).await {
Ok(b) => b,
Err(e) => {
tracing::error!("Failed to connect to remote server: {}", e);
let _ = playwright.shutdown().await;
let _ = server_process.kill().await;
panic!("Connect failed: {:?}", e);
}
};
tracing::info!("Connected! Browser GUID: {}", browser.guid());
assert!(browser.is_connected());
assert!(!browser.version().is_empty());
tracing::info!("Browser version: {}", browser.version());
let context = browser
.new_context()
.await
.expect("Failed to create context");
let page = context.new_page().await.expect("Failed to create page");
page.goto("data:text/html,<h1>Hello from Remote!</h1>", None)
.await
.expect("Failed to navigate");
let locator = page.locator("h1").await;
let content = locator.text_content().await.expect("Failed to get text");
assert_eq!(
content,
Some("Hello from Remote!".to_string()),
"Page content should match"
);
tracing::info!("✓ Remote connection test passed!");
page.close().await.ok();
context.close().await.ok();
browser.close().await.ok();
playwright.shutdown().await.ok();
let _ = server_process.kill().await;
}
#[tokio::test]
async fn test_connect_timeout_when_server_unavailable() {
crate::common::init_tracing();
let playwright = match Playwright::launch().await {
Ok(p) => p,
Err(e) => {
tracing::warn!("Skipping test: Failed to launch Playwright: {}", e);
return;
}
};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener); let ws_endpoint = format!("ws://127.0.0.1:{}", port);
tracing::info!(
"Attempting to connect to unavailable server at {}",
ws_endpoint
);
let start = std::time::Instant::now();
let options = playwright_rs::api::ConnectOptions::new().timeout(2000.0);
let result = playwright
.chromium()
.connect(&ws_endpoint, Some(options))
.await;
let elapsed = start.elapsed();
assert!(result.is_err(), "Expected connection to fail");
assert!(
elapsed < Duration::from_secs(5),
"Connection took too long to fail: {:?}",
elapsed
);
tracing::info!(
"✓ Connection failed as expected in {:?}: {:?}",
elapsed,
result.unwrap_err()
);
playwright.shutdown().await.ok();
}
#[tokio::test]
async fn test_connect_with_custom_headers() {
crate::common::init_tracing();
let drivers_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("drivers");
let package_path = std::fs::read_dir(&drivers_dir)
.ok()
.and_then(|mut entries| entries.next())
.and_then(|e| e.ok())
.map(|e| e.path().join("package"));
let package_path = match package_path {
Some(p) if p.exists() => p,
_ => {
tracing::warn!("Skipping test: Playwright driver not found");
return;
}
};
let (mut server_process, ws_endpoint) = match start_browser_server(&package_path).await {
Some(result) => result,
None => {
tracing::warn!("Skipping test: Failed to start browser server");
return;
}
};
let playwright = match Playwright::launch().await {
Ok(p) => p,
Err(e) => {
let _ = server_process.kill().await;
tracing::warn!("Skipping test: {}", e);
return;
}
};
let mut headers = std::collections::HashMap::new();
headers.insert("X-Custom-Auth".to_string(), "test-token".to_string());
headers.insert("X-Request-Id".to_string(), "12345".to_string());
let options = playwright_rs::api::ConnectOptions::new()
.headers(headers)
.timeout(10000.0);
let browser = match playwright
.chromium()
.connect(&ws_endpoint, Some(options))
.await
{
Ok(b) => b,
Err(e) => {
tracing::error!("Failed to connect: {}", e);
let _ = playwright.shutdown().await;
let _ = server_process.kill().await;
panic!("Connect with headers failed: {:?}", e);
}
};
assert!(browser.is_connected());
tracing::info!("✓ Connected with custom headers successfully");
browser.close().await.ok();
playwright.shutdown().await.ok();
let _ = server_process.kill().await;
}