Web Driver

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

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');
}

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: true untuk CI/CD — browser tanpa UI jauh lebih cepat dan tidak membutuhkan display server.
  • Jangan gunakan Future.delayed sebagai pengganti tunggu — gunakan polling adaptif yang menunggu kondisi terpenuhi, lebih cepat dan andal.
  • data-testid attribute 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 tearDown jika 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 CIselenium/standalone-chrome image 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.

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

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