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
Navigasi Browser #
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_insangat mudah dibaca.- Headless Chrome untuk CI/CD — tambahkan
--headless=new --no-sandbox --disable-dev-shm-usageagar berjalan di lingkungan tanpa display server.- Explicit Wait, bukan
sleep—Selenium::WebDriver::WaitatauCapybara.default_max_wait_timelebih andal darisleep;sleepmembuat 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_elementstidak raise error — berbeda darifind_element; gunakanfind_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: truehanya 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.
webdriversgem untuk manajemen driver — mengelola download dan update ChromeDriver/GeckoDriver secara otomatis; tidak perlu install manual.
← Sebelumnya: ORM Adapter Berikutnya: Artikel & Sumber Daya →