Web Driver #
WebDriver adalah protokol standar W3C untuk mengontrol browser web secara programatik — memungkinkan menulis script Dart yang membuka browser sungguhan, mengklik tombol, mengisi form, dan memverifikasi konten halaman. Di Dart, package webdriver mengimplementasikan protokol ini dan bisa digunakan bersama ChromeDriver (Chrome) atau GeckoDriver (Firefox). Penggunaan utama: pengujian end-to-end (E2E) untuk aplikasi web, web scraping dari halaman yang butuh JavaScript, dan otomasi tugas berulang di browser.
WebDriver vs Puppeteer/Playwright #
flowchart LR
D["Dart webdriver\n(W3C WebDriver Protocol)"] --> CD["ChromeDriver\n(Chrome)"]
D --> GD["GeckoDriver\n(Firefox)"]
D --> ED["EdgeDriver\n(Edge)"]
P["Puppeteer/Playwright\n(Chrome DevTools Protocol)"] --> C["Chrome/Chromium"]
| Aspek | Dart webdriver |
Puppeteer/Playwright |
|---|---|---|
| Protokol | W3C WebDriver (standar) | CDP (Chrome-specific untuk Puppeteer) |
| Browser | Chrome, Firefox, Edge, Safari | Chrome/Chromium, Firefox, WebKit |
| Bahasa | Dart | JavaScript/TypeScript, Python, dll. |
| Kecepatan | Sedikit lebih lambat | Lebih cepat (CDP lebih langsung) |
| Standar | ✓ W3C standar | CDP non-standar (kecuali Playwright) |
| Cocok untuk | E2E test Dart, integrasi Flutter web | Scraping cepat, monitoring |
Setup #
1. Tambahkan Package #
dart pub add webdriver
# pubspec.yaml
dependencies:
webdriver: ^3.0.3
2. Download WebDriver #
ChromeDriver — sesuaikan versi dengan Chrome yang terinstall:
# Cek versi Chrome
google-chrome --version
# atau di macOS: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version
# Download ChromeDriver dari https://chromedriver.chromium.org/downloads
# Atau gunakan webdriver-manager:
npm install -g webdriver-manager
webdriver-manager update
webdriver-manager start --chrome
GeckoDriver (Firefox):
# Download dari https://github.com/mozilla/geckodriver/releases
# Ekstrak dan tambahkan ke PATH
3. Jalankan Driver #
# ChromeDriver — jalankan sebelum script Dart
chromedriver --port=4444
# GeckoDriver
geckodriver --port=4444
# Atau via Docker (lebih mudah untuk CI)
docker run -d -p 4444:4444 --shm-size="2g" \
selenium/standalone-chrome:latest
Koneksi dan Konfigurasi #
import 'package:webdriver/async_io.dart';
Future<WebDriver> buatDriver({
bool headless = false,
String browser = 'chrome',
}) async {
final capabilities = <String, dynamic>{};
if (browser == 'chrome') {
final chromeOptions = <String, dynamic>{
'args': [
if (headless) '--headless', // jalankan tanpa UI
'--no-sandbox', // diperlukan di Linux CI
'--disable-dev-shm-usage', // menghindari crash di container
'--window-size=1920,1080',
'--disable-gpu',
],
'prefs': {
'download.default_directory': '/tmp/downloads',
},
};
capabilities['goog:chromeOptions'] = chromeOptions;
} else if (browser == 'firefox') {
capabilities['moz:firefoxOptions'] = {
'args': [if (headless) '-headless'],
};
}
final driver = await createDriver(
uri: Uri.parse('http://localhost:4444/wd/hub'),
desired: capabilities,
);
// Konfigurasi timeout default
await driver.timeouts.setImplicitTimeout(Duration(seconds: 10));
await driver.timeouts.setPageLoadTimeout(Duration(seconds: 30));
await driver.timeouts.setScriptTimeout(Duration(seconds: 10));
return driver;
}
Future<void> main() async {
final driver = await buatDriver(headless: true);
try {
await demo(driver);
} finally {
// Selalu tutup driver!
await driver.quit();
}
}
Navigasi Browser #
import 'package:webdriver/async_io.dart';
Future<void> navigasi(WebDriver driver) async {
// Buka URL
await driver.get('https://example.com');
// Dapatkan URL saat ini
print(await driver.currentUrl); // 'https://example.com'
// Dapatkan judul halaman
print(await driver.title); // 'Example Domain'
// Navigasi maju/mundur seperti browser
await driver.back();
await driver.forward();
// Refresh halaman
await driver.refresh();
// Dapatkan source HTML halaman
final source = await driver.pageSource;
print(source.substring(0, 200));
// Ukuran window
await driver.window.setSize(Rectangle(0, 0, 1440, 900));
await driver.window.maximize();
// Screenshot halaman penuh
final screenshot = await driver.captureScreenshotAsBase64();
await File('screenshot.png').writeAsBytes(base64Decode(screenshot));
}
Menemukan Elemen #
WebDriver menemukan elemen menggunakan berbagai strategi selector:
import 'package:webdriver/async_io.dart';
Future<void> cariElemen(WebDriver driver) async {
await driver.get('https://example.com/login');
// Strategi pencarian elemen
// 1. By ID — paling cepat dan andal
final emailInput = await driver.findElement(By.id('email'));
// 2. By CSS Selector — fleksibel
final tombolLogin = await driver.findElement(By.cssSelector('#submit-btn'));
final header = await driver.findElement(By.cssSelector('h1.title'));
final namaKelas = await driver.findElement(By.cssSelector('.form-control.email'));
// 3. By XPath — powerful tapi verbose
final linkDaftar = await driver.findElement(
By.xpath('//a[contains(text(), "Daftar")]'),
);
// 4. By Name — untuk input form
final passwordInput = await driver.findElement(By.name('password'));
// 5. By Tag Name
final semuaButton = await driver.findElements(By.tagName('button'));
// 6. By Class Name
final errorMessages = await driver.findElements(By.className('error-msg'));
// 7. By Link Text
final lupaPassword = await driver.findElement(By.linkText('Lupa Password?'));
// Cari elemen dalam elemen lain
final form = await driver.findElement(By.id('login-form'));
final inputDalamForm = await form.findElements(By.tagName('input'));
// Cek apakah elemen ada tanpa throw
try {
final opsional = await driver.findElement(By.id('mungkin-tidak-ada'));
print('Elemen ada: ${await opsional.text}');
} on NoSuchElementException {
print('Elemen tidak ditemukan');
}
// Tunggu elemen muncul (lihat bagian Menunggu)
print('Jumlah button: ${semuaButton.length}');
}
Interaksi dengan Elemen #
Future<void> interaksiElemen(WebDriver driver) async {
await driver.get('https://example.com/form');
// Klik elemen
final tombol = await driver.findElement(By.id('submit'));
await tombol.click();
// Isi input teks
final namaInput = await driver.findElement(By.id('nama'));
await namaInput.clear(); // bersihkan dulu
await namaInput.sendKeys('Budi Santoso');
// Isi input dengan keyboard special keys
final searchInput = await driver.findElement(By.name('q'));
await searchInput.sendKeys('dart programming');
await searchInput.sendKeys(Keys.RETURN); // tekan Enter
// Dropdown / Select
final dropdown = await driver.findElement(By.id('kategori'));
// Pilih by value
await driver.execute(
"arguments[0].value = arguments[1]",
[dropdown, 'elektronik'],
);
// Atau klik option langsung
final option = await dropdown.findElement(By.cssSelector('option[value="elektronik"]'));
await option.click();
// Checkbox
final checkbox = await driver.findElement(By.id('setuju'));
if (!(await checkbox.selected)) {
await checkbox.click();
}
// Upload file
final fileInput = await driver.findElement(By.id('file-upload'));
await fileInput.sendKeys('/path/ke/file.pdf'); // path absolut di mesin lokal
// Hover — untuk trigger dropdown atau tooltip
final actions = driver.mouse;
await actions.moveTo(element: tombol);
await actions.click();
// Scroll ke elemen
await driver.execute('arguments[0].scrollIntoView(true)', [tombol]);
// Baca atribut dan properti elemen
final href = await driver.findElement(By.tagName('a'))
.then((el) => el.attributes['href']);
final isDisabled = await tombol.attributes['disabled'];
final teks = await tombol.text;
final nama = await tombol.name;
print('Href: $href, Disabled: $isDisabled, Teks: $teks');
}
Menunggu Kondisi Dinamis #
Halaman web modern sering memuat konten secara asinkron — ini adalah salah satu tantangan terbesar dalam WebDriver. Jangan gunakan sleep — gunakan polling yang cerdas:
import 'package:webdriver/async_io.dart';
import 'dart:async';
// Tunggu hingga kondisi terpenuhi dengan timeout
Future<T> tungguHingga<T>({
required Future<T?> Function() kondisi,
Duration timeout = const Duration(seconds: 30),
Duration pollingInterval = const Duration(milliseconds: 500),
String? pesanTimeout,
}) async {
final batas = DateTime.now().add(timeout);
while (DateTime.now().isBefore(batas)) {
try {
final hasil = await kondisi();
if (hasil != null) return hasil;
} catch (_) {
// Abaikan exception selama polling
}
await Future.delayed(pollingInterval);
}
throw TimeoutException(
pesanTimeout ?? 'Kondisi tidak terpenuhi dalam ${timeout.inSeconds} detik',
);
}
// Implementasi kondisi-kondisi umum
Future<WebElement> tungguElemen(
WebDriver driver,
By by, {
Duration timeout = const Duration(seconds: 30),
}) async {
return tungguHingga(
kondisi: () async {
try {
final el = await driver.findElement(by);
if (await el.displayed) return el;
return null;
} on NoSuchElementException {
return null;
}
},
timeout: timeout,
pesanTimeout: 'Elemen $by tidak muncul dalam ${timeout.inSeconds} detik',
);
}
Future<void> tungguTeks(
WebDriver driver,
By by,
String teks, {
Duration timeout = const Duration(seconds: 30),
}) async {
await tungguHingga(
kondisi: () async {
final el = await driver.findElement(by);
final elTeks = await el.text;
return elTeks.contains(teks) ? elTeks : null;
},
timeout: timeout,
pesanTimeout: 'Teks "$teks" tidak muncul di $by',
);
}
Future<void> tungguHilang(
WebDriver driver,
By by, {
Duration timeout = const Duration(seconds: 30),
}) async {
await tungguHingga(
kondisi: () async {
try {
await driver.findElement(by);
return null; // masih ada
} on NoSuchElementException {
return true; // sudah hilang
}
},
timeout: timeout,
pesanTimeout: 'Elemen $by tidak hilang dalam ${timeout.inSeconds} detik',
);
}
// Penggunaan
Future<void> loginDanTunggu(WebDriver driver) async {
await driver.get('https://example.com/login');
await (await driver.findElement(By.id('email'))).sendKeys('[email protected]');
await (await driver.findElement(By.id('password'))).sendKeys('password');
await (await driver.findElement(By.id('submit'))).click();
// Tunggu spinner loading hilang
await tungguHilang(driver, By.cssSelector('.loading-spinner'));
// Tunggu pesan sukses muncul
await tungguTeks(driver, By.cssSelector('.flash-message'), 'Login berhasil');
// Pastikan navigasi ke dashboard
await tungguHingga(
kondisi: () async {
final url = await driver.currentUrl;
return url.contains('/dashboard') ? url : null;
},
pesanTimeout: 'Tidak berhasil masuk ke dashboard',
);
}
End-to-End Testing #
WebDriver sangat cocok untuk E2E testing aplikasi web:
import 'package:test/test.dart';
import 'package:webdriver/async_io.dart';
void main() {
late WebDriver driver;
setUpAll(() async {
driver = await buatDriver(headless: true);
});
tearDownAll(() async {
await driver.quit();
});
// Screenshot saat test gagal
tearDown(() async {
if (hasTestFailures) {
final testName = currentTestName.replaceAll(' ', '_');
final screenshot = await driver.captureScreenshotAsBase64();
await File('test_failures/$testName.png').writeAsBytes(base64Decode(screenshot));
}
});
group('Alur Login', () {
test('login dengan kredensial valid', () async {
await driver.get('http://localhost:3000/login');
await (await driver.findElement(By.id('email')))
.sendKeys('[email protected]');
await (await driver.findElement(By.id('password')))
.sendKeys('admin123');
await (await driver.findElement(By.cssSelector('button[type=submit]')))
.click();
await tungguHingga(
kondisi: () async {
final url = await driver.currentUrl;
return url.contains('/dashboard') ? url : null;
},
);
final judul = await driver.title;
expect(judul, contains('Dashboard'));
});
test('tampilkan error untuk email tidak valid', () async {
await driver.get('http://localhost:3000/login');
await (await driver.findElement(By.id('email')))
.sendKeys('bukan-email');
await (await driver.findElement(By.cssSelector('button[type=submit]')))
.click();
final errorEl = await tungguElemen(driver, By.cssSelector('.error-email'));
final pesanError = await errorEl.text;
expect(pesanError, contains('Email tidak valid'));
});
});
group('CRUD Produk', () {
setUp(() async {
await loginSebagaiAdmin(driver);
});
test('buat produk baru', () async {
await driver.get('http://localhost:3000/admin/produk/baru');
await (await driver.findElement(By.name('nama')))
.sendKeys('Produk Test E2E');
await (await driver.findElement(By.name('harga')))
.sendKeys('99000');
await (await driver.findElement(By.cssSelector('button.simpan')))
.click();
await tungguTeks(
driver,
By.cssSelector('.flash-sukses'),
'Produk berhasil disimpan',
);
});
});
}
Web Scraping dengan WebDriver #
Future<List<Map<String, String>>> scraperBeritaTerkini(
WebDriver driver,
String url,
) async {
await driver.get(url);
// Scroll ke bawah untuk load infinite scroll
for (int i = 0; i < 3; i++) {
await driver.execute('window.scrollTo(0, document.body.scrollHeight)', []);
await Future.delayed(Duration(seconds: 2));
}
final artikel = <Map<String, String>>[];
final items = await driver.findElements(By.cssSelector('article.news-item'));
for (final item in items) {
final judul = await item.findElement(By.cssSelector('h2'));
final link = await item.findElement(By.cssSelector('a'));
final tanggal = await item.findElement(By.cssSelector('time'));
artikel.add({
'judul': await judul.text,
'url': await link.attributes['href'] ?? '',
'tanggal': await tanggal.attributes['datetime'] ?? '',
});
}
return artikel;
}
Menjalankan JavaScript #
Future<void> jalankanJS(WebDriver driver) async {
// Eksekusi JavaScript di browser
await driver.execute('console.log("Halo dari Dart!")', []);
// Ambil nilai dari JavaScript
final tinggi = await driver.execute(
'return document.body.scrollHeight',
[],
) as int;
print('Tinggi halaman: $tinggi px');
// Modifikasi DOM via JavaScript
await driver.execute(
'document.getElementById("target").style.border = "3px solid red"',
[],
);
// Scroll ke posisi tertentu
await driver.execute('window.scrollTo(0, arguments[0])', [500]);
// Tunggu kondisi via JavaScript
await driver.execute(
'return arguments[0].complete',
[await driver.findElement(By.tagName('img'))],
);
}
Anti-Pattern WebDriver #
Menggunakan Sleep Statis #
// ANTI-PATTERN: sleep yang tidak adaptif
await (await driver.findElement(By.id('submit'))).click();
await Future.delayed(Duration(seconds: 5)); // ✗ selalu tunggu 5 detik
final hasil = await driver.findElement(By.id('result')).then((e) => e.text);
// BENAR: tunggu hingga kondisi terpenuhi
await (await driver.findElement(By.id('submit'))).click();
final hasil = await tungguElemen(driver, By.id('result')); // ✓ adaptif
print(await hasil.text);
Selector yang Rapuh #
// ANTI-PATTERN: selector yang bergantung pada struktur DOM yang bisa berubah
final tombol = await driver.findElement(
By.xpath('/html/body/div[2]/div[1]/form/div[3]/button[1]'),
); // ✗ rapuh — berubah jika HTML berubah sedikit saja
// BENAR: selector yang bermakna dan stabil
// Tambahkan data-testid ke elemen di HTML: <button data-testid="login-submit">
final tombol = await driver.findElement(
By.cssSelector('[data-testid="login-submit"]'),
); // ✓ stabil dan bermakna
Tidak Menutup Driver #
// ANTI-PATTERN: lupa tutup driver — proses browser menggantung
Future<void> main() async {
final driver = await buatDriver();
await driver.get('https://example.com');
// ✗ tidak memanggil driver.quit() — ChromeDriver terus berjalan!
}
// BENAR: selalu tutup dengan try-finally
Future<void> main() async {
final driver = await buatDriver();
try {
await driver.get('https://example.com');
} finally {
await driver.quit(); // ✓ selalu ditutup meski ada exception
}
}
Ringkasan #
- ChromeDriver/GeckoDriver harus berjalan sebelum script Dart dieksekusi — driver adalah proses terpisah yang menjembatani Dart dan browser.
- Gunakan
headless: trueuntuk CI/CD — browser tanpa UI jauh lebih cepat dan tidak membutuhkan display server.- Jangan gunakan
Future.delayedsebagai pengganti tunggu — gunakan polling adaptif yang menunggu kondisi terpenuhi, lebih cepat dan andal.data-testidattribute adalah cara terbaik untuk membuat selector yang stabil — tidak bergantung pada struktur CSS atau HTML yang sering berubah.- Selalu tutup driver di
finally— lupa menutup driver menyebabkan proses browser terus berjalan dan menghabiskan memori.- Screenshot saat test gagal sangat membantu debugging — simpan screenshot di
tearDownjika test gagal untuk melihat kondisi halaman saat itu.- XPath powerful tapi rapuh — gunakan CSS selector untuk pencarian umum, XPath hanya untuk kasus yang tidak bisa dilakukan dengan CSS (seperti mencari parent element).
- Gunakan Docker untuk CI —
selenium/standalone-chromeimage memberikan environment yang konsisten tanpa perlu install ChromeDriver manual.- WebDriver cocok untuk E2E testing tapi terlalu lambat untuk unit/integration testing — gunakan hanya untuk pengujian alur bisnis yang harus diuji di browser sungguhan.
- Web scraping dengan WebDriver lebih lambat dari HTTP + HTML parsing, tapi diperlukan untuk halaman yang membutuhkan JavaScript execution atau autentikasi yang kompleks.