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"]| Aspect | Dart webdriver | Puppeteer/Playwright |
|---|---|---|
| Protocol | W3C WebDriver (standard) | CDP (Chrome-specific for Puppeteer) |
| Browsers | Chrome, Firefox, Edge, Safari | Chrome/Chromium, Firefox, WebKit |
| Language | Dart | JavaScript/TypeScript, Python, etc. |
| Speed | Slightly slower | Faster (CDP is more direct) |
| Standard | ✓ W3C standard | Non-standard CDP (except Playwright) |
| Best for | Dart E2E tests, Flutter web integration | Fast 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: truefor CI/CD — browsers without a UI are much faster and don’t need a display server.- Don’t use
Future.delayedas a substitute for waiting — use adaptive polling that waits for a condition to be met; it’s faster and more reliable.data-testidattributes 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
tearDownwhen 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-chromeimage 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.