Selenium

Selenium #

Selenium WebDriver adalah standar industri untuk otomatisasi browser — memungkinkan kode Ruby mengontrol Chrome, Firefox, Safari, atau Edge layaknya pengguna nyata: membuka halaman, mengklik tombol, mengisi form, menekan Enter, dan memverifikasi konten yang tampil. Di Ruby, Selenium digunakan untuk dua keperluan utama: integration testing (memastikan alur aplikasi web bekerja dari sudut pandang pengguna) dan web scraping (mengekstrak data dari halaman yang membutuhkan JavaScript untuk merender konten). Dengan gem selenium-webdriver dan capybara, Ruby punya ekosistem browser automation yang matang dan banyak digunakan di aplikasi Rails produksi.

Selenium vs Capybara vs Ferrum #

selenium-webdriver (langsung):
  ✓ Kontrol penuh atas browser
  ✓ Cocok untuk web scraping kompleks
  ✗ API verbose, perlu banyak kode boilerplate
  ✗ Kurang idiomatik untuk testing Rails

Capybara (di atas Selenium):
  ✓ DSL yang sangat ekspresif dan readable
  ✓ Terintegrasi sempurna dengan RSpec dan Rails
  ✓ Bisa ganti driver (Rack::Test, Selenium, Cuprite) tanpa ubah test
  ✓ Wait otomatis — tidak perlu sleep eksplisit
  ✓ Standar de-facto untuk integration test Rails
  ✗ Abstraksi mengurangi kontrol langsung

Ferrum (Chrome DevTools Protocol):
  ✓ Tidak butuh ChromeDriver — langsung ke Chrome via CDP
  ✓ Lebih cepat dari Selenium
  ✓ Screenshot, PDF, network interception
  ✗ Hanya Chrome/Chromium, tidak multi-browser

Instalasi #

# Install ChromeDriver (pastikan versi cocok dengan Chrome yang terinstal)
# macOS
brew install chromedriver

# Ubuntu
sudo apt install chromium-chromedriver

# Atau biarkan webdrivers gem yang manage otomatis
gem install selenium-webdriver webdrivers
gem install capybara   # untuk integration test Rails
# Gemfile
gem 'selenium-webdriver', '~> 4.15'
gem 'webdrivers',         '~> 5.3'    # auto-download dan manage driver

# Untuk testing Rails
group :test do
  gem 'capybara',           '~> 3.39'
  gem 'selenium-webdriver', '~> 4.15'
  gem 'webdrivers',         '~> 5.3'
end

Memulai dengan Selenium WebDriver #

require 'selenium-webdriver'
require 'webdrivers'   # auto-manage ChromeDriver

# Buat instance driver Chrome
driver = Selenium::WebDriver.for(:chrome)

# Atau headless (tanpa tampilkan browser) — untuk CI/CD
opsi = Selenium::WebDriver::Chrome::Options.new
opsi.add_argument("--headless=new")
opsi.add_argument("--no-sandbox")
opsi.add_argument("--disable-dev-shm-usage")
opsi.add_argument("--window-size=1920,1080")

driver = Selenium::WebDriver.for(:chrome, options: opsi)

# Firefox
driver = Selenium::WebDriver.for(:firefox)

# Selalu tutup browser setelah selesai
begin
  driver.navigate.to("https://example.com")
  puts driver.title
ensure
  driver.quit
end

driver = Selenium::WebDriver.for(:chrome, options: opsi_headless)

# Navigasi ke URL
driver.navigate.to("https://ruby-lang.org")
driver.get("https://ruby-lang.org")   # alias yang lebih singkat

# Navigasi sejarah
driver.navigate.back
driver.navigate.forward
driver.navigate.refresh

# Informasi halaman
puts driver.current_url
puts driver.title

# Ukuran dan posisi window
driver.manage.window.maximize
driver.manage.window.resize_to(1440, 900)
driver.manage.window.position = Selenium::WebDriver::Point.new(0, 0)

# Fullscreen
driver.manage.window.full_screen

# Screenshot halaman
driver.save_screenshot("screenshot.png")
screenshot_base64 = driver.screenshot_as(:base64)

# Execute JavaScript
driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")
judul = driver.execute_script("return document.title")
puts judul

# Tunggu halaman selesai load
driver.manage.timeouts.page_load = 30   # maksimal 30 detik tunggu load

Menemukan Elemen #

Selenium menyediakan berbagai strategi untuk menemukan elemen di halaman:

# find_element — temukan satu elemen (raise jika tidak ditemukan)
# find_elements — temukan semua elemen (kembalikan array kosong jika tidak ada)

# By ID — paling cepat dan direkomendasikan
elemen = driver.find_element(id: "tombol-kirim")

# By Name
elemen = driver.find_element(name: "email")

# By Class Name
elemen = driver.find_element(class: "btn-primary")

# By Tag Name
elemen = driver.find_element(tag_name: "h1")

# By CSS Selector — paling fleksibel
elemen = driver.find_element(css: "#form-login .btn-submit")
elemen = driver.find_element(css: "input[type='email']")
elemen = driver.find_element(css: "ul.produk-list > li:first-child")

# By XPath — paling powerful tapi paling verbose
elemen = driver.find_element(xpath: "//button[@type='submit']")
elemen = driver.find_element(xpath: "//h1[contains(text(), 'Selamat Datang')]")
elemen = driver.find_element(xpath: "//table//tr[3]/td[2]")

# By Link Text — untuk elemen <a>
elemen = driver.find_element(link_text: "Login")
elemen = driver.find_element(partial_link_text: "Daft")  # partial match

# Temukan semua elemen yang cocok
semua_produk = driver.find_elements(css: ".produk-card")
puts "Jumlah produk: #{semua_produk.length}"

semua_produk.each do |kartu|
  nama  = kartu.find_element(css: ".nama-produk").text
  harga = kartu.find_element(css: ".harga").text
  puts "#{nama}: #{harga}"
end

# Cek apakah elemen ada tanpa raise
def elemen_ada?(driver, css)
  driver.find_elements(css: css).any?
rescue Selenium::WebDriver::Error::NoSuchElementError
  false
end

Berinteraksi dengan Elemen #

# Informasi elemen
puts elemen.text               # teks yang terlihat
puts elemen.tag_name           # "input", "button", "a", dll.
puts elemen.attribute("href")  # nilai atribut HTML
puts elemen.attribute("class")
puts elemen.css_value("color") # nilai CSS property
puts elemen.displayed?         # apakah terlihat oleh user
puts elemen.enabled?           # apakah aktif (tidak disabled)
puts elemen.selected?          # untuk checkbox/radio

# Klik
tombol = driver.find_element(css: "#tombol-kirim")
tombol.click

# Input teks
input_email = driver.find_element(css: "input[name='email']")
input_email.send_keys("[email protected]")   # ketik teks
input_email.clear                            # hapus isi
input_email.send_keys("[email protected]")

# Shortcut keyboard
input_cari = driver.find_element(css: "#cari")
input_cari.send_keys("ruby programming")
input_cari.send_keys(:return)   # tekan Enter
input_cari.send_keys(:tab)      # tekan Tab

# Kombinasi tombol
input_cari.send_keys([:control, "a"])  # Ctrl+A (select all)
input_cari.send_keys([:control, "c"])  # Ctrl+C (copy)

# Dropdown / Select
require 'selenium-webdriver'
select_kota = Selenium::WebDriver::Support::Select.new(
  driver.find_element(id: "pilih-kota")
)
select_kota.select_by(:text, "Bandung")    # pilih berdasarkan teks
select_kota.select_by(:value, "bdg")       # pilih berdasarkan value
select_kota.select_by(:index, 2)           # pilih berdasarkan indeks

puts select_kota.first_selected_option.text  # opsi yang dipilih saat ini
puts select_kota.options.map(&:text).inspect # semua opsi

# Checkbox
checkbox = driver.find_element(id: "setuju-syarat")
checkbox.click unless checkbox.selected?   # centang jika belum

# Upload file
input_file = driver.find_element(css: "input[type='file']")
input_file.send_keys(File.expand_path("~/dokumen/cv.pdf"))

Actions API — Interaksi Tingkat Lanjut #

# Actions API untuk interaksi kompleks: hover, drag-drop, right-click
aksi = driver.action

# Hover / Mouse over
elemen = driver.find_element(css: ".menu-dropdown")
aksi.move_to(elemen).perform
# Tunggu dropdown muncul
sleep 0.5
item = driver.find_element(css: ".menu-dropdown .item-pertama")
item.click

# Right-click (context menu)
aksi.context_click(elemen).perform

# Double click
aksi.double_click(elemen).perform

# Drag and Drop
sumber = driver.find_element(id: "item-draggable")
target = driver.find_element(id: "drop-zone")
aksi.drag_and_drop(sumber, target).perform

# Klik di koordinat tertentu
aksi.move_to_location(500, 300).click.perform

# Scroll ke elemen
driver.execute_script("arguments[0].scrollIntoView(true)", elemen)
# atau:
aksi.scroll_to_element(elemen).perform

# Menahan Shift sambil klik (untuk multi-select)
aksi.key_down(:shift).click(elemen2).key_up(:shift).perform

Waiting — Menunggu Elemen Siap #

Ini adalah bagian terpenting dalam menulis Selenium yang andal — selalu gunakan wait daripada sleep:

require 'selenium-webdriver'

driver = Selenium::WebDriver.for(:chrome, options: opsi)

# Implicit Wait — tunggu sampai N detik untuk semua pencarian elemen
driver.manage.timeouts.implicit_wait = 10   # 10 detik
# Setelah ini, semua find_element otomatis menunggu sampai 10 detik

# Explicit Wait — tunggu kondisi spesifik terpenuhi
tunggu = Selenium::WebDriver::Wait.new(timeout: 15, interval: 0.5)

# Tunggu sampai elemen ada di DOM
elemen = tunggu.until { driver.find_element(css: "#hasil-pencarian") }

# Tunggu sampai elemen terlihat
tunggu.until { driver.find_element(css: ".loading-spinner").displayed? == false }

# Tunggu kondisi kustom
tunggu.until do
  jumlah = driver.find_elements(css: ".produk-card").length
  jumlah > 0
end

# Tunggu URL berubah (setelah redirect)
tunggu.until { driver.current_url.include?("/dashboard") }

# Tunggu judul berubah
tunggu.until { driver.title.include?("Berhasil Login") }

# Dengan pesan error yang informatif
tunggu = Selenium::WebDriver::Wait.new(
  timeout: 15,
  message: "Tombol kirim tidak muncul setelah 15 detik"
)
tombol = tunggu.until { driver.find_element(css: "#btn-kirim") }

# Expected Conditions (lebih ekspresif)
kondisi = Selenium::WebDriver::Support::ExpectedConditions
tunggu.until { kondisi.element_to_be_clickable(driver.find_element(id: "tombol")) }

Frame dan Window #

# FRAME / IFRAME — beralih ke frame sebelum berinteraksi dengan isinya
iframe = driver.find_element(tag_name: "iframe")
driver.switch_to.frame(iframe)
# Sekarang bisa berinteraksi dengan elemen di dalam iframe
driver.find_element(css: "#konten-iframe").text

# Kembali ke halaman utama dari frame
driver.switch_to.default_content

# Beralih ke frame berdasarkan indeks
driver.switch_to.frame(0)   # frame pertama

# WINDOW / TAB — beralih antar jendela
jendela_utama = driver.window_handle

# Klik link yang buka tab baru
driver.find_element(css: "a[target='_blank']").click

# Dapatkan semua window handle
semua_jendela = driver.window_handles
puts "Total jendela: #{semua_jendela.length}"

# Beralih ke jendela baru (tab terakhir)
driver.switch_to.window(semua_jendela.last)
puts "URL tab baru: #{driver.current_url}"

# Tutup tab saat ini dan kembali ke jendela utama
driver.close
driver.switch_to.window(jendela_utama)

# Alert / Confirm / Prompt
driver.find_element(css: "#btn-hapus").click

alert = driver.switch_to.alert
puts alert.text   # "Apakah kamu yakin ingin menghapus?"
alert.accept      # klik OK
# alert.dismiss   # klik Cancel

# Prompt dengan input
prompt = driver.switch_to.alert
prompt.send_keys("input_untuk_prompt")
prompt.accept

Web Scraping dengan Selenium #

Selenium sangat berguna untuk scraping halaman yang merender konten dengan JavaScript:

require 'selenium-webdriver'
require 'webdrivers'
require 'json'

opsi = Selenium::WebDriver::Chrome::Options.new
opsi.add_argument("--headless=new")
opsi.add_argument("--no-sandbox")
opsi.add_argument("--disable-dev-shm-usage")
opsi.add_argument("--user-agent=Mozilla/5.0 (compatible; RubyBot/1.0)")

driver = Selenium::WebDriver.for(:chrome, options: opsi)
tunggu = Selenium::WebDriver::Wait.new(timeout: 15)

begin
  driver.get("https://toko-online.example.com/produk")

  # Tunggu produk muncul setelah JS render
  tunggu.until { driver.find_elements(css: ".produk-card").any? }

  # Scroll ke bawah untuk load more (infinite scroll)
  3.times do
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")
    sleep 1.5
  end

  # Ekstrak data
  produk_list = driver.find_elements(css: ".produk-card").map do |kartu|
    {
      nama:   kartu.find_element(css: ".nama").text,
      harga:  kartu.find_element(css: ".harga").text.gsub(/[Rp.,\s]/, "").to_i,
      rating: kartu.find_element(css: ".rating").text.to_f,
      url:    kartu.find_element(css: "a").attribute("href")
    }
  rescue Selenium::WebDriver::Error::NoSuchElementError
    nil
  end.compact

  # Simpan ke JSON
  File.write("produk.json", JSON.pretty_generate(produk_list))
  puts "Berhasil scrape #{produk_list.length} produk"

ensure
  driver.quit
end

Capybara — Integration Testing yang Idiomatik #

Capybara menyediakan DSL yang jauh lebih nyaman untuk integration testing Rails:

# spec/rails_helper.rb atau spec/spec_helper.rb
require 'capybara/rails'
require 'capybara/rspec'
require 'selenium-webdriver'
require 'webdrivers'

# Konfigurasi driver headless Chrome
Capybara.register_driver(:chrome_headless) do |app|
  opsi = Selenium::WebDriver::Chrome::Options.new
  opsi.add_argument("--headless=new")
  opsi.add_argument("--no-sandbox")
  opsi.add_argument("--disable-dev-shm-usage")
  opsi.add_argument("--window-size=1440,900")

  Capybara::Selenium::Driver.new(
    app,
    browser:  :chrome,
    options:  opsi
  )
end

Capybara.default_driver      = :rack_test        # untuk test tanpa JS
Capybara.javascript_driver   = :chrome_headless  # untuk test dengan JS
Capybara.default_max_wait_time = 10              # tunggu otomatis 10 detik
Capybara.server              = :puma, { Silent: true }
# spec/features/login_spec.rb
require 'rails_helper'

RSpec.describe "Login", type: :feature do
  let(:user) { create(:user, email: "[email protected]", password: "password123") }

  it "berhasil login dengan kredensial yang benar" do
    visit login_path

    fill_in "Email", with: user.email
    fill_in "Password", with: "password123"
    click_button "Masuk"

    expect(page).to have_content("Selamat datang, #{user.nama}!")
    expect(page).to have_current_path(dashboard_path)
  end

  it "gagal login dengan password salah" do
    visit login_path

    fill_in "Email", with: user.email
    fill_in "Password", with: "password_salah"
    click_button "Masuk"

    expect(page).to have_content("Email atau password tidak valid")
    expect(page).to have_current_path(login_path)
  end

  it "redirect ke halaman login jika belum login" do
    visit dashboard_path
    expect(page).to have_current_path(login_path)
  end
end

# spec/features/checkout_spec.rb
RSpec.describe "Proses Checkout", type: :feature, js: true do
  # js: true → gunakan chrome_headless driver

  let(:user)   { create(:user) }
  let(:produk) { create(:produk, nama: "Laptop Gaming", harga: 15_000_000, stok: 5) }

  before do
    login_as(user, scope: :user)   # dengan devise helpers
  end

  it "berhasil checkout produk" do
    visit produk_path(produk)

    click_button "Tambah ke Keranjang"
    expect(page).to have_content("Produk berhasil ditambahkan")

    visit keranjang_path
    click_button "Lanjut ke Pembayaran"

    expect(page).to have_content("Ringkasan Pesanan")
    expect(page).to have_content("Laptop Gaming")
    expect(page).to have_content("Rp 15.000.000")

    fill_in "Nama Penerima",  with: "Rina Wijaya"
    fill_in "Alamat",         with: "Jl. Sudirman No. 1, Bandung"
    select  "Transfer Bank",  from: "Metode Pembayaran"

    click_button "Konfirmasi Pesanan"

    # Tunggu redirect dan konfirmasi
    expect(page).to have_content("Pesanan Berhasil Dibuat", wait: 10)
    expect(Pesanan.last.pengguna).to eq(user)
  end
end

Matcher Capybara yang Sering Digunakan #

# Cek konten
expect(page).to have_content("Teks yang dicari")
expect(page).to have_text("Teks ini")

# Cek CSS selector
expect(page).to have_css(".kelas-elemen")
expect(page).to have_css("#id-elemen")
expect(page).to have_css("button", text: "Kirim")
expect(page).to have_no_css(".error-message")

# Cek link
expect(page).to have_link("Klik di sini")
expect(page).to have_link("Klik", href: "/halaman")

# Cek button
expect(page).to have_button("Kirim")
expect(page).to have_button("Kirim", disabled: true)

# Cek field
expect(page).to have_field("Email")
expect(page).to have_field("Email", with: "[email protected]")
expect(page).to have_select("Kota", selected: "Bandung")
expect(page).to have_checked_field("Setuju dengan syarat")

# Cek URL
expect(page).to have_current_path("/dashboard")
expect(page).to have_current_path(%r{/produk/\d+})

# Tindakan Capybara
visit "/halaman"
click_link "Daftar"
click_button "Kirim"
fill_in "Email", with: "[email protected]"
select "Bandung", from: "Kota"
check "Setuju"
uncheck "Newsletter"
choose "Bayar dengan Transfer"   # radio button
attach_file "Foto KTP", Rails.root.join("spec/fixtures/foto.jpg")

Page Object Model — Struktur Test yang Rapi #

Page Object Model (POM) memisahkan detail halaman dari logika test:

# spec/support/pages/login_page.rb
class LoginPage
  include Capybara::DSL

  def visit_page
    visit login_path
    self
  end

  def isi_email(email)
    fill_in "Email", with: email
    self
  end

  def isi_password(password)
    fill_in "Password", with: password
    self
  end

  def klik_login
    click_button "Masuk"
    self
  end

  def login_dengan(email:, password:)
    isi_email(email).isi_password(password).klik_login
  end

  def pesan_error
    find(".pesan-error").text
  end

  def berhasil?
    page.has_content?("Selamat datang")
  end
end

# spec/features/login_spec.rb
RSpec.describe "Login", type: :feature do
  let(:halaman_login) { LoginPage.new }
  let(:user) { create(:user, email: "[email protected]", password: "password123") }

  it "berhasil login" do
    halaman_login
      .visit_page
      .login_dengan(email: user.email, password: "password123")

    expect(halaman_login).to be_berhasil
  end

  it "gagal dengan password salah" do
    halaman_login
      .visit_page
      .login_dengan(email: user.email, password: "salah")

    expect(halaman_login.pesan_error).to include("tidak valid")
  end
end

Menjalankan di CI/CD #

# .github/workflows/test.yml
name: Integration Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5          

    steps:
      - uses: actions/checkout@v4

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Install Chrome
        uses: browser-actions/setup-chrome@latest

      - name: Setup Database
        run: bundle exec rails db:create db:schema:load

      - name: Run System Tests
        run: bundle exec rspec spec/features/
        env:
          RAILS_ENV: test
          DATABASE_URL: postgresql://postgres:postgres@localhost/test_db

Ringkasan #

  • Gunakan Capybara untuk integration test Rails — API-nya jauh lebih ekspresif dari selenium-webdriver langsung; have_content, click_button, fill_in sangat mudah dibaca.
  • Headless Chrome untuk CI/CD — tambahkan --headless=new --no-sandbox --disable-dev-shm-usage agar berjalan di lingkungan tanpa display server.
  • Explicit Wait, bukan sleepSelenium::WebDriver::Wait atau Capybara.default_max_wait_time lebih andal dari sleep; sleep membuat test lambat dan tetap bisa flaky.
  • CSS Selector lebih baik dari XPath — lebih mudah dibaca dan lebih cepat; gunakan XPath hanya ketika CSS selector tidak bisa mengekspresikan kondisi yang dibutuhkan (misalnya “teks yang mengandung”).
  • find_elements tidak raise error — berbeda dari find_element; gunakan find_elements(...).any? untuk mengecek keberadaan elemen tanpa risiko exception.
  • Page Object Model untuk test yang berkembang — pisahkan detail DOM (CSS selector, cara navigasi) dari logika test; ketika halaman berubah, hanya Page Object yang perlu diupdate.
  • js: true hanya jika butuh JavaScript — test dengan JS driver (Selenium) jauh lebih lambat dari Rack::Test; gunakan hanya untuk fitur yang memang butuh JS.
  • Tangani StaleElementReferenceError — elemen bisa “basi” jika DOM diupdate setelah element ditemukan; cari ulang elemen setelah aksi yang mengubah DOM.
  • Screenshot saat test gagal — konfigurasi Capybara untuk otomatis mengambil screenshot saat test gagal; sangat membantu debug flaky test di CI.
  • webdrivers gem untuk manajemen driver — mengelola download dan update ChromeDriver/GeckoDriver secara otomatis; tidak perlu install manual.

← Sebelumnya: ORM Adapter   Berikutnya: Artikel & Sumber Daya →

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