Selenium RS

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: Done

WebDriver 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(())
}

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 pattern let result = operasi().await; driver.quit().await?; result untuk memastikan cleanup selalu terjadi.
  • Gunakan query(...).wait(timeout, interval) bukan sleep — explicit wait jauh lebih andal dari fixed sleep. Tunggu kondisi spesifik, bukan waktu yang ditebak.
  • Headless mode untuk server dan CIcaps.set_headless()? + --no-sandbox + --disable-dev-shm-usage adalah 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::Css paling 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-chrome Docker image dan konfigurasi URL driver dari environment variable.
  • thirtyfour vs fantoccini — keduanya adalah WebDriver client untuk Rust; thirtyfour lebih ergonomis dan aktif dikembangkan; fantoccini lebih minimalis. Untuk kebanyakan kasus, thirtyfour adalah pilihan yang lebih baik.

← Sebelumnya: Diesel   Berikutnya: Article & Sumber Daya →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact