Selenium RS #
Browser automation di Rust menggunakan crate thirtyfour — implementasi WebDriver protocol yang async dan ergonomis. Nama “Selenium RS” pada situs ini merujuk ke ekosistem browser automation di Rust secara umum, dengan thirtyfour sebagai pilihan utamanya karena API yang bersih dan dukungan async native. Browser automation berguna untuk dua tujuan utama: end-to-end testing (memverifikasi aplikasi web dari sudut pandang pengguna nyata) dan web scraping (mengekstrak data dari halaman yang membutuhkan JavaScript untuk render kontennya). Artikel ini membahas setup, semua operasi penting, pola pengujian yang idiomatis, dan pertimbangan headless mode untuk CI/CD.
Arsitektur WebDriver #
sequenceDiagram
participant R as Rust Program\n(thirtyfour)
participant D as WebDriver\n(ChromeDriver/GeckoDriver)
participant B as Browser\n(Chrome/Firefox)
R->>D: HTTP: POST /session (capabilities)
D->>B: Launch browser
B->>D: Browser ready
D->>R: session_id
R->>D: HTTP: POST /session/{id}/url
D->>B: Navigate to URL
B->>D: Page loaded
R->>D: HTTP: POST /session/{id}/element
D->>B: Find element
B->>D: element_id
R->>D: HTTP: POST /element/{id}/click
D->>B: Click element
B->>D: DoneWebDriver adalah standar W3C — kamu menulis kode sekali dan bisa dijalankan di Chrome, Firefox, Edge, atau Safari dengan mengubah driver-nya saja.
Prasyarat dan Instalasi #
Sebelum menjalankan kode, pastikan WebDriver sudah terinstal:
# Chrome — download ChromeDriver yang cocok dengan versi Chrome kamu
# https://chromedriver.chromium.org/downloads
# atau gunakan chromedriver-autoinstall
# Firefox — GeckoDriver
# https://github.com/mozilla/geckodriver/releases
# Jalankan WebDriver di terminal terpisah
chromedriver --port=4444
# atau
geckodriver --port=4444
[dependencies]
thirtyfour = "0.32"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Koneksi dan Navigasi Dasar #
use thirtyfour::prelude::*;
#[tokio::main]
async fn main() -> WebDriverResult<()> {
// Setup capabilities
let mut caps = DesiredCapabilities::chrome();
// Headless mode — tidak buka jendela browser (untuk server/CI)
caps.set_headless()?;
caps.add_chrome_arg("--no-sandbox")?;
caps.add_chrome_arg("--disable-dev-shm-usage")?;
caps.add_chrome_arg("--window-size=1920,1080")?;
// Hubungkan ke ChromeDriver yang sudah berjalan
let driver = WebDriver::new("http://localhost:4444", caps).await?;
// Navigasi ke URL
driver.goto("https://www.contoh.com").await?;
println!("URL: {}", driver.current_url().await?);
println!("Judul: {}", driver.title().await?);
// Navigasi back/forward
driver.back().await?;
driver.forward().await?;
// Refresh halaman
driver.refresh().await?;
// Ambil HTML seluruh halaman
let source = driver.source().await?;
println!("Panjang HTML: {} karakter", source.len());
// Tutup browser — PENTING: selalu tutup di akhir
driver.quit().await?;
Ok(())
}
Menemukan Elemen #
use thirtyfour::prelude::*;
async fn contoh_find(driver: &WebDriver) -> WebDriverResult<()> {
driver.goto("https://www.contoh.com").await?;
// By ID
let elemen = driver.find(By::Id("tombol-utama")).await?;
// By CSS selector — paling fleksibel
let judul = driver.find(By::Css("h1.judul-utama")).await?;
let tombol = driver.find(By::Css("button[type='submit']")).await?;
// By XPath — powerful tapi verbose
let link = driver.find(By::XPath("//a[@href='/tentang']")).await?;
// By tag name
let semua_h2: Vec<WebElement> = driver.find_all(By::Tag("h2")).await?;
println!("Jumlah H2: {}", semua_h2.len());
// By link text
let link_kontak = driver.find(By::LinkText("Hubungi Kami")).await?;
// By partial link text
let link_partial = driver.find(By::PartialLinkText("Tentang")).await?;
// Elemen di dalam elemen (relative search)
let form = driver.find(By::Id("form-login")).await?;
let input_email = form.find(By::Name("email")).await?;
// Cek keberadaan elemen tanpa error
let mungkin_ada = driver.find(By::Id("mungkin-tidak-ada")).await;
match mungkin_ada {
Ok(el) => println!("Elemen ada: {}", el.id().await?.unwrap_or_default()),
Err(_) => println!("Elemen tidak ditemukan"),
}
Ok(())
}
Interaksi dengan Elemen #
use thirtyfour::prelude::*;
async fn contoh_interaksi(driver: &WebDriver) -> WebDriverResult<()> {
driver.goto("https://contoh.com/login").await?;
// Klik elemen
let tombol = driver.find(By::Css("button.buka-menu")).await?;
tombol.click().await?;
// Input teks
let email_input = driver.find(By::Id("email")).await?;
email_input.clear().await?; // bersihkan dulu
email_input.send_keys("[email protected]").await?;
let password_input = driver.find(By::Id("password")).await?;
password_input.send_keys("rahasia123").await?;
// Kirim form dengan Enter
password_input.send_keys(Key::Return).await?;
// atau klik tombol submit
driver.find(By::Css("button[type='submit']")).await?.click().await?;
// Ambil teks dari elemen
let pesan = driver.find(By::Css(".pesan-sukses")).await?;
println!("Pesan: {}", pesan.text().await?);
// Ambil attribute
let link = driver.find(By::Css("a.link-utama")).await?;
let href = link.attr("href").await?.unwrap_or_default();
println!("Href: {}", href);
// Ambil nilai input
let nilai_input: String = driver
.find(By::Id("nama-depan"))
.await?
.attr("value")
.await?
.unwrap_or_default();
// Cek status elemen
let tombol_hapus = driver.find(By::Id("tombol-hapus")).await?;
println!("Enabled: {}", tombol_hapus.is_enabled().await?);
println!("Displayed: {}", tombol_hapus.is_displayed().await?);
println!("Selected: {}", tombol_hapus.is_selected().await?);
Ok(())
}
Dropdown, Checkbox, dan Select #
use thirtyfour::prelude::*;
async fn contoh_form_lanjutan(driver: &WebDriver) -> WebDriverResult<()> {
driver.goto("https://contoh.com/form").await?;
// Checkbox — cek status dan toggle
let checkbox = driver.find(By::Id("setuju-syarat")).await?;
if !checkbox.is_selected().await? {
checkbox.click().await?; // centang
}
println!("Checkbox checked: {}", checkbox.is_selected().await?);
// Radio button
let radio_ya = driver.find(By::Css("input[type='radio'][value='ya']")).await?;
radio_ya.click().await?;
// <select> dropdown
let select_el = driver.find(By::Id("pilih-kota")).await?;
// Select by visible text
let select = thirtyfour::extensions::form::SelectElement::new(&select_el).await?;
select.select_by_visible_text("Jakarta").await?;
// Select by value attribute
select.select_by_value("surabaya").await?;
// Select by index
select.select_by_index(2).await?;
// Ambil opsi yang dipilih
let opsi_terpilih = select.first_selected_option().await?;
println!("Terpilih: {}", opsi_terpilih.text().await?);
// File upload
let file_input = driver.find(By::Id("upload-foto")).await?;
file_input.send_keys("/path/to/foto.jpg").await?;
Ok(())
}
Wait Strategies — Menunggu Elemen #
Ini adalah aspek paling penting dari browser automation yang stabil. Jangan pernah menggunakan sleep yang fixed — gunakan explicit wait:
use thirtyfour::prelude::*;
use std::time::Duration;
async fn contoh_wait(driver: &WebDriver) -> WebDriverResult<()> {
driver.goto("https://contoh.com/async-load").await?;
// ANTI-PATTERN: sleep fixed — rapuh dan lambat
// tokio::time::sleep(Duration::from_secs(3)).await;
// let el = driver.find(By::Id("konten")).await?;
// BENAR: wait until — tunggu kondisi terpenuhi
// Timeout 10 detik, polling setiap 500ms
let el = driver
.query(By::Id("konten-dimuat"))
.wait(Duration::from_secs(10), Duration::from_millis(500))
.first()
.await?;
// Wait until element terlihat
let loading = driver.find(By::Css(".loading-spinner")).await.ok();
if let Some(spinner) = loading {
// Tunggu sampai spinner hilang
spinner
.wait_until()
.displayed(false)
.await?;
}
// Wait until teks berubah
let status = driver.find(By::Id("status-proses")).await?;
status
.wait_until()
.condition(Box::new(|el: &WebElement| {
Box::pin(async move {
let teks = el.text().await?;
Ok(teks == "Selesai" || teks == "Error")
})
}))
.await?;
println!("Status akhir: {}", status.text().await?);
Ok(())
}
Frame, Window, dan Tab #
use thirtyfour::prelude::*;
async fn contoh_frame_window(driver: &WebDriver) -> WebDriverResult<()> {
driver.goto("https://contoh.com").await?;
// Bekerja dengan iframe
let iframe = driver.find(By::Id("embedded-content")).await?;
driver.enter_frame(Some(&iframe)).await?;
// Sekarang bisa akses konten di dalam iframe
let konten = driver.find(By::Css(".konten-iframe")).await?;
println!("Konten iframe: {}", konten.text().await?);
// Kembali ke frame utama
driver.enter_parent_frame().await?;
// atau
driver.enter_default_frame().await?;
// Buka tab baru
driver.execute("window.open('https://contoh2.com', '_blank');", vec![]).await?;
// Daftar semua window/tab
let windows = driver.windows().await?;
println!("Jumlah tab: {}", windows.len());
// Pindah ke tab baru (biasanya yang terakhir)
driver.switch_to_window(windows.last().unwrap().clone()).await?;
println!("URL tab baru: {}", driver.current_url().await?);
// Kembali ke tab pertama
driver.switch_to_window(windows.first().unwrap().clone()).await?;
// Tutup tab saat ini
driver.close_window().await?;
Ok(())
}
Screenshot dan JavaScript #
use thirtyfour::prelude::*;
async fn contoh_screenshot_js(driver: &WebDriver) -> WebDriverResult<()> {
driver.goto("https://contoh.com").await?;
// Screenshot seluruh halaman
let screenshot = driver.screenshot_as_png().await?;
std::fs::write("screenshot.png", &screenshot)?;
println!("Screenshot disimpan: {} byte", screenshot.len());
// Screenshot satu elemen
let elemen = driver.find(By::Id("konten-utama")).await?;
let el_screenshot = elemen.screenshot_as_png().await?;
std::fs::write("elemen.png", &el_screenshot)?;
// Eksekusi JavaScript
let title: serde_json::Value = driver
.execute("return document.title;", vec![])
.await?
.json()?;
println!("Judul via JS: {}", title);
// Scroll ke elemen
let footer = driver.find(By::Tag("footer")).await?;
driver
.execute("arguments[0].scrollIntoView(true);", vec![footer.to_json()?])
.await?;
// Set nilai input via JavaScript (untuk field yang sulit di-interact)
let input = driver.find(By::Id("input-readonly")).await?;
driver
.execute(
"arguments[0].value = arguments[1];",
vec![input.to_json()?, serde_json::Value::String("nilai baru".to_string())],
)
.await?;
// Klik via JavaScript (berguna jika elemen tertutup elemen lain)
driver
.execute("arguments[0].click();", vec![input.to_json()?])
.await?;
// Ambil localStorage
let token: serde_json::Value = driver
.execute("return localStorage.getItem('auth_token');", vec![])
.await?
.json()?;
println!("Token: {}", token);
Ok(())
}
End-to-End Testing #
use thirtyfour::prelude::*;
use std::time::Duration;
#[tokio::test]
async fn test_alur_login() -> WebDriverResult<()> {
let mut caps = DesiredCapabilities::chrome();
caps.set_headless()?;
caps.add_chrome_arg("--no-sandbox")?;
caps.add_chrome_arg("--disable-dev-shm-usage")?;
let driver = WebDriver::new("http://localhost:4444", caps).await?;
// Pastikan browser ditutup bahkan jika test gagal
let result = test_login_internal(&driver).await;
driver.quit().await?;
result
}
async fn test_login_internal(driver: &WebDriver) -> WebDriverResult<()> {
driver.goto("http://localhost:3000/login").await?;
// Isi form login
driver.find(By::Id("email")).await?
.send_keys("[email protected]").await?;
driver.find(By::Id("password")).await?
.send_keys("password123").await?;
driver.find(By::Css("button[type='submit']")).await?
.click().await?;
// Tunggu redirect ke dashboard
driver
.query(By::Css(".dashboard-container"))
.wait(Duration::from_secs(5), Duration::from_millis(200))
.first()
.await?;
// Verifikasi redirect berhasil
let url = driver.current_url().await?;
assert!(url.contains("/dashboard"), "Seharusnya di dashboard, tapi: {}", url);
// Verifikasi elemen ada di halaman
let pesan_sambutan = driver.find(By::Css(".sambutan-pengguna")).await?;
assert!(pesan_sambutan.text().await?.contains("admin"),
"Pesan sambutan tidak mengandung nama pengguna");
Ok(())
}
// Test halaman
#[tokio::test]
async fn test_halaman_tidak_ditemukan() -> WebDriverResult<()> {
let mut caps = DesiredCapabilities::chrome();
caps.set_headless()?;
let driver = WebDriver::new("http://localhost:4444", caps).await?;
driver.goto("http://localhost:3000/halaman-tidak-ada").await?;
let judul = driver.title().await?;
assert_eq!(judul, "404 - Tidak Ditemukan");
driver.quit().await?;
Ok(())
}
Web Scraping #
use thirtyfour::prelude::*;
#[derive(Debug)]
struct HasilScraping {
judul: String,
harga: String,
rating: Option<String>,
}
async fn scrape_produk(driver: &WebDriver, url: &str) -> WebDriverResult<Vec<HasilScraping>> {
driver.goto(url).await?;
// Scroll sampai ke bawah untuk load lazy content
for _ in 0..5 {
driver.execute("window.scrollBy(0, 800);", vec![]).await?;
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// Ambil semua produk
let produk_elements = driver.find_all(By::Css(".produk-card")).await?;
let mut hasil = Vec::new();
for el in &produk_elements {
let judul = el.find(By::Css(".produk-judul")).await?
.text().await?;
let harga = el.find(By::Css(".produk-harga")).await?
.text().await?;
// Rating mungkin tidak selalu ada
let rating = el.find(By::Css(".produk-rating")).await
.ok()
.map(|r| async move { r.text().await })
.map(|f| tokio::runtime::Handle::current().block_on(f))
.and_then(|r| r.ok());
hasil.push(HasilScraping { judul, harga, rating });
}
println!("Ditemukan {} produk", hasil.len());
Ok(hasil)
}
Konfigurasi untuk CI/CD #
# Docker Compose untuk CI/CD dengan Selenium Grid
# docker-compose.yml untuk testing
services:
chrome:
image: selenium/standalone-chrome:latest
ports:
- "4444:4444"
shm_size: "2gb"
environment:
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
test:
build: .
depends_on:
- chrome
environment:
- WEBDRIVER_URL=http://chrome:4444
command: cargo test --test e2e_tests
// Ambil URL WebDriver dari environment variable untuk fleksibilitas
async fn buat_driver() -> WebDriverResult<WebDriver> {
let url = std::env::var("WEBDRIVER_URL")
.unwrap_or_else(|_| "http://localhost:4444".to_string());
let mut caps = DesiredCapabilities::chrome();
caps.set_headless()?;
caps.add_chrome_arg("--no-sandbox")?;
caps.add_chrome_arg("--disable-dev-shm-usage")?;
caps.add_chrome_arg("--window-size=1920,1080")?;
WebDriver::new(&url, caps).await
}
Ringkasan #
- Selalu
driver.quit()di akhir — tutup sesi WebDriver bahkan jika terjadi error. Gunakan patternlet result = operasi().await; driver.quit().await?; resultuntuk memastikan cleanup selalu terjadi.- Gunakan
query(...).wait(timeout, interval)bukansleep— explicit wait jauh lebih andal dari fixed sleep. Tunggu kondisi spesifik, bukan waktu yang ditebak.- Headless mode untuk server dan CI —
caps.set_headless()?+--no-sandbox+--disable-dev-shm-usageadalah kombinasi standar untuk lingkungan tanpa display.- CSS selector lebih stabil dari XPath — gunakan class, ID, dan attribute yang memiliki makna semantik. Hindari XPath berbasis urutan elemen yang rapuh.
- Screenshot saat test gagal — ambil screenshot ketika assertion gagal untuk memudahkan debugging. Simpan dengan nama yang mengandung timestamp atau nama test.
By::Csspaling fleksibel — mendukung semua selector CSS termasuk:nth-child,[attribute], kombinasi class, dan pseudo-class.- Scraping JavaScript-heavy page — scroll untuk trigger lazy loading, tunggu network idle, dan gunakan
driver.execute()untuk berinteraksi dengan state yang tidak bisa diakses via DOM biasa.- Gunakan Selenium Grid via Docker — untuk parallel testing di CI, gunakan
selenium/standalone-chromeDocker image dan konfigurasi URL driver dari environment variable.thirtyfourvsfantoccini— keduanya adalah WebDriver client untuk Rust;thirtyfourlebih ergonomis dan aktif dikembangkan;fantoccinilebih minimalis. Untuk kebanyakan kasus,thirtyfouradalah pilihan yang lebih baik.