Web Driver #

WebDriver is the W3C standard protocol for programmatically controlling web browsers — letting you write Dart scripts that open a real browser, click buttons, fill forms, and verify page content. In Dart, the webdriver package implements this protocol and works with ChromeDriver (Chrome) or GeckoDriver (Firefox). Main use cases: end-to-end (E2E) testing for web apps, web scraping of JavaScript-heavy pages, and automating repetitive browser tasks.

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"]
AspectDart webdriverPuppeteer/Playwright
ProtocolW3C WebDriver (standard)CDP (Chrome-specific for Puppeteer)
BrowsersChrome, Firefox, Edge, SafariChrome/Chromium, Firefox, WebKit
LanguageDartJavaScript/TypeScript, Python, etc.
SpeedSlightly slowerFaster (CDP is more direct)
Standard✓ W3C standardNon-standard CDP (except Playwright)
Best forDart E2E tests, Flutter web integrationFast scraping, monitoring

Setup #

1. Add the Package #

dart pub add webdriver
# pubspec.yaml
dependencies:
  webdriver: ^3.0.3

2. Download a WebDriver #

ChromeDriver — match the version to your installed Chrome:

# Check the Chrome version
google-chrome --version
# or on macOS: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version

# Download ChromeDriver from https://chromedriver.chromium.org/downloads
# Or use webdriver-manager:
npm install -g webdriver-manager
webdriver-manager update
webdriver-manager start --chrome

GeckoDriver (Firefox):

# Download from https://github.com/mozilla/geckodriver/releases
# Extract and add to PATH

3. Run the Driver #

# ChromeDriver — run before the Dart script
chromedriver --port=4444

# GeckoDriver
geckodriver --port=4444

# Or via Docker (easier for CI)
docker run -d -p 4444:4444 --shm-size="2g" \
  selenium/standalone-chrome:latest

Connection and Configuration #

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',        // run without a UI
        '--no-sandbox',                     // required on Linux CI
        '--disable-dev-shm-usage',         // avoid crashes in containers
        '--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,
  );

  // Configure default timeouts
  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 {
    // Always close the driver!
    await driver.quit();
  }
}

Browser Navigation #

import 'package:webdriver/async_io.dart';

Future<void> navigasi(WebDriver driver) async {
  // Open a URL
  await driver.get('https://example.com');

  // Get the current URL
  print(await driver.currentUrl); // 'https://example.com'

  // Get the page title
  print(await driver.title); // 'Example Domain'

  // Navigate forward/back like a browser
  await driver.back();
  await driver.forward();

  // Refresh the page
  await driver.refresh();

  // Get the page's HTML source
  final source = await driver.pageSource;
  print(source.substring(0, 200));

  // Window size
  await driver.window.setSize(Rectangle(0, 0, 1440, 900));
  await driver.window.maximize();

  // Full-page screenshot
  final screenshot = await driver.captureScreenshotAsBase64();
  await File('screenshot.png').writeAsBytes(base64Decode(screenshot));
}

Finding Elements #

WebDriver finds elements using various selector strategies:

import 'package:webdriver/async_io.dart';

Future<void> cariElemen(WebDriver driver) async {
  await driver.get('https://example.com/login');

  // Element search strategies
  // 1. By ID — fastest and most reliable
  final emailInput = await driver.findElement(By.id('email'));

  // 2. By CSS Selector — flexible
  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 but verbose
  final linkDaftar = await driver.findElement(
    By.xpath('//a[contains(text(), "Daftar")]'),
  );

  // 4. By Name — for form inputs
  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?'));

  // Search for elements within other elements
  final form = await driver.findElement(By.id('login-form'));
  final inputDalamForm = await form.findElements(By.tagName('input'));

  // Check whether an element exists without throwing
  try {
    final opsional = await driver.findElement(By.id('mungkin-tidak-ada'));
    print('Element exists: ${await opsional.text}');
  } on NoSuchElementException {
    print('Element not found');
  }

  // Wait for the element to appear (see the Waiting section)
  print('Button count: ${semuaButton.length}');
}

Interacting with Elements #

Future<void> interaksiElemen(WebDriver driver) async {
  await driver.get('https://example.com/form');

  // Click an element
  final tombol = await driver.findElement(By.id('submit'));
  await tombol.click();

  // Fill a text input
  final namaInput = await driver.findElement(By.id('nama'));
  await namaInput.clear();         // clear it first
  await namaInput.sendKeys('Budi Santoso');

  // Fill an input with special keyboard keys
  final searchInput = await driver.findElement(By.name('q'));
  await searchInput.sendKeys('dart programming');
  await searchInput.sendKeys(Keys.RETURN);  // press Enter

  // Dropdown / Select
  final dropdown = await driver.findElement(By.id('kategori'));
  // Select by value
  await driver.execute(
    "arguments[0].value = arguments[1]",
    [dropdown, 'elektronik'],
  );
  // Or click the option directly
  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();
  }

  // File upload
  final fileInput = await driver.findElement(By.id('file-upload'));
  await fileInput.sendKeys('/path/ke/file.pdf'); // absolute path on the local machine

  // Hover — to trigger a dropdown or tooltip
  final actions = driver.mouse;
  await actions.moveTo(element: tombol);
  await actions.click();

  // Scroll to an element
  await driver.execute('arguments[0].scrollIntoView(true)', [tombol]);

  // Read element attributes and properties
  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, Text: $teks');
}

Waiting for Dynamic Conditions #

Modern web pages often load content asynchronously — this is one of the biggest challenges in WebDriver. Don’t use sleep — use smart polling:

import 'package:webdriver/async_io.dart';
import 'dart:async';

// Wait until a condition is met with a 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 (_) {
      // Ignore exceptions during polling
    }
    await Future.delayed(pollingInterval);
  }

  throw TimeoutException(
    pesanTimeout ?? 'Condition not met within ${timeout.inSeconds} seconds',
  );
}

// Implementations of common conditions
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: 'Element $by didn't appear within ${timeout.inSeconds} seconds',
  );
}

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: 'Text "$teks" didn't appear in $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; // still there
      } on NoSuchElementException {
        return true; // gone
      }
    },
    timeout: timeout,
    pesanTimeout: 'Element $by didn't disappear within ${timeout.inSeconds} seconds',
  );
}

// Usage
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();

  // Wait for the loading spinner to disappear
  await tungguHilang(driver, By.cssSelector('.loading-spinner'));

  // Wait for the success message to appear
  await tungguTeks(driver, By.cssSelector('.flash-message'), 'Login berhasil');

  // Make sure navigation to the dashboard happened
  await tungguHingga(
    kondisi: () async {
      final url = await driver.currentUrl;
      return url.contains('/dashboard') ? url : null;
    },
    pesanTimeout: 'Failed to reach the dashboard',
  );
}

End-to-End Testing #

WebDriver is perfect for E2E testing web apps:

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 on test failure
  tearDown(() async {
    if (hasTestFailures) {
      final testName = currentTestName.replaceAll(' ', '_');
      final screenshot = await driver.captureScreenshotAsBase64();
      await File('test_failures/$testName.png').writeAsBytes(base64Decode(screenshot));
    }
  });

  group('Login Flow', () {
    test('login with valid credentials', () 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('shows an error for an invalid email', () 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('Product CRUD', () {
    setUp(() async {
      await loginSebagaiAdmin(driver);
    });

    test('creates a new product', () 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 with WebDriver #

Future<List<Map<String, String>>> scraperBeritaTerkini(
  WebDriver driver,
  String url,
) async {
  await driver.get(url);

  // Scroll down to load infinite scroll content
  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;
}

Running JavaScript #

Future<void> jalankanJS(WebDriver driver) async {
  // Execute JavaScript in the browser
  await driver.execute('console.log("Hello from Dart!")', []);

  // Get a value from JavaScript
  final tinggi = await driver.execute(
    'return document.body.scrollHeight',
    [],
  ) as int;
  print('Page height: $tinggi px');

  // Modify the DOM via JavaScript
  await driver.execute(
    'document.getElementById("target").style.border = "3px solid red"',
    [],
  );

  // Scroll to a specific position
  await driver.execute('window.scrollTo(0, arguments[0])', [500]);

  // Wait for a condition via JavaScript
  await driver.execute(
    'return arguments[0].complete',
    [await driver.findElement(By.tagName('img'))],
  );
}

WebDriver Anti-Patterns #

Using Static Sleep #

// ANTI-PATTERN: non-adaptive sleep
await (await driver.findElement(By.id('submit'))).click();
await Future.delayed(Duration(seconds: 5)); // ✗ always waits 5 seconds
final hasil = await driver.findElement(By.id('result')).then((e) => e.text);

// CORRECT: wait until the condition is met
await (await driver.findElement(By.id('submit'))).click();
final hasil = await tungguElemen(driver, By.id('result')); // ✓ adaptive
print(await hasil.text);

Fragile Selectors #

// ANTI-PATTERN: selectors depending on a DOM structure that can change
final tombol = await driver.findElement(
  By.xpath('/html/body/div[2]/div[1]/form/div[3]/button[1]'),
); // ✗ fragile — breaks if the HTML changes even slightly

// CORRECT: meaningful, stable selectors
// Add data-testid to elements in the HTML: <button data-testid="login-submit">
final tombol = await driver.findElement(
  By.cssSelector('[data-testid="login-submit"]'),
); // ✓ stable and meaningful

Not Closing the Driver #

// ANTI-PATTERN: forgetting to close the driver — browser processes hang around
Future<void> main() async {
  final driver = await buatDriver();
  await driver.get('https://example.com');
  // ✗ no driver.quit() call — ChromeDriver keeps running!
}

// CORRECT: always close with try-finally
Future<void> main() async {
  final driver = await buatDriver();
  try {
    await driver.get('https://example.com');
  } finally {
    await driver.quit(); // ✓ always closed even on exceptions
  }
}

Summary #

  • ChromeDriver/GeckoDriver must be running before the Dart script executes — the driver is a separate process bridging Dart and the browser.
  • Use headless: true for CI/CD — browsers without a UI are much faster and don’t need a display server.
  • Don’t use Future.delayed as a substitute for waiting — use adaptive polling that waits for a condition to be met; it’s faster and more reliable.
  • data-testid attributes are the best way to create stable selectors — they don’t depend on frequently changing CSS or HTML structures.
  • Always close the driver in finally — forgetting to close the driver leaves browser processes running and consuming memory.
  • Screenshots on test failure are a huge debugging help — save a screenshot in tearDown when a test fails to see the page’s state at that moment.
  • XPath is powerful but fragile — use CSS selectors for general searches, XPath only for cases CSS can’t handle (like finding a parent element).
  • Use Docker for CI — the selenium/standalone-chrome image provides a consistent environment without manually installing ChromeDriver.
  • WebDriver is good for E2E testing but too slow for unit/integration testing — use it only for business flows that must be tested in a real browser.
  • Web scraping with WebDriver is slower than HTTP + HTML parsing, but necessary for pages requiring JavaScript execution or complex authentication.

← Previous: Dart ORM   Next: Articles & Resources →

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