Appium Flutter Driver is a test automation tool for Flutter apps on multiple platforms/OSes. Appium Flutter Driver is part of the Appium mobile test automation tool maintained by community. Feel free to create PRs to fix issues/improve this driver.
This package is in early stage of experiment, breaking changes and breaking codes are to be expected! All contributions, including non-code, are welcome! See TODO list below.
Even though Flutter comes with superb integration test support, Flutter Driver, it does not fit some specific use cases, such as
- writing test in other languages than Dart
- running integration test for Flutter app with embedded webview or native view, or existing native app with embedded Flutter view
- running test on multiple devices simultaneously
- running integration test on device farms, such as Sauce Labs, AWS, Firebase
Under the hood, Appium Flutter Driver use the Dart VM Service Protocol with extension ext.flutter.driver
, similar to Flutter Driver, to control the Flutter app-under-test (AUT).
In order to use appium-flutter-driver
, we need to use appium
version 1.16.0
or higher
npm i -g appium-flutter-driver
If you are unfamiliar with running Appium tests, start with Appium Getting Starting first.
Your Flutter app-under-test (AUT) must be compiled in debug
or profile
mode, because Flutter Driver does not support running in release mode.
. Also, ensure that your Flutter AUT has enableFlutterDriverExtension()
before runApp
. Then, please make sure your app imported flutter_driver
package as well.
This snippet, taken from example dir, is a script written as an appium client with webdriverio
, and assumes you have appium
server (with appium-flutter-driver
installed) running on the same host and default port (4723
). For more info, see example's README.md
Capability | Description | Example Values |
---|---|---|
retryBackoffTime | the time wait for socket connection retry for get flutter session (default 3000ms) | 500 |
maxRetryCount | the count for socket connection retry for get flutter session (default 30) | 20 |
const wdio = require('webdriverio');
const assert = require('assert');
const { byValueKey } = require('appium-flutter-finder');
const osSpecificOps = process.env.APPIUM_OS === 'android' ? {
platformName: 'Android',
deviceName: 'Pixel 2',
// @todo support non-unix style path
app: __dirname + '/../apps/app-free-debug.apk',
}: process.env.APPIUM_OS === 'ios' ? {
platformName: 'iOS',
platformVersion: '12.2',
deviceName: 'iPhone X',
noReset: true,
app: __dirname + '/../apps/Runner.zip',
} : {};
const opts = {
port: 4723,
capabilities: {
...osSpecificOps,
automationName: 'Flutter',
retryBackoffTime: 500
}
};
(async () => {
const counterTextFinder = byValueKey('counter');
const buttonFinder = byValueKey('increment');
const driver = await wdio.remote(opts);
if (process.env.APPIUM_OS === 'android') {
await driver.switchContext('NATIVE_APP');
await (await driver.$('~fab')).click();
await driver.switchContext('FLUTTER');
} else {
console.log('Switching context to `NATIVE_APP` is currently only applicable to Android demo app.')
}
assert.strictEqual(await driver.getElementText(counterTextFinder), '0');
await driver.elementClick(buttonFinder);
await driver.touchAction({
action: 'tap',
element: { elementId: buttonFinder }
});
assert.strictEqual(await driver.getElementText(counterTextFinder), '2');
driver.deleteSession();
})();
Legend:
Icon | Description |
---|---|
✅ | integrated to CI |
🆗 | manual tested without CI |
availalbe without manual tested | |
❌ | unavailable |
Flutter Driver API | Status | WebDriver example |
---|---|---|
ancestor | 🆗 | |
bySemanticsLabel | 🆗 | |
byTooltip | 🆗 | byTooltip('Increment') |
byType | 🆗 | byType('TextField') |
byValueKey | 🆗 | byValueKey('counter') |
descendant | 🆗 | |
pageBack | 🆗 | pageBack() |
text | 🆗 | byText('foo') |
The below WebDriver example is by webdriverio.
flutter:
prefix commands are mobile:
command in appium for Android and iOS.
Please replace them properly with your client.
Flutter API | Status | WebDriver example | Scope |
---|---|---|---|
FlutterDriver.connectedTo | 🆗 | wdio.remote(opts) |
Session |
checkHealth | 🆗 | driver.execute('flutter:checkHealth') |
Session |
clearTextbox | 🆗 | driver.elementClear(find.byType('TextField')) |
Session |
clearTimeline | 🆗 | driver.execute('flutter:clearTimeline') |
Session |
close | 🆗 | driver.deleteSession() |
Session |
enterText | 🆗 | driver.elementSendKeys(find.byType('TextField'), 'I can enter text') (no focus required) driver.elementClick(find.byType('TextField')); driver.execute('flutter:enterText', 'I can enter text') (focus required by tap/click first) |
Session |
forceGC | 🆗 | driver.execute('flutter:forceGC') |
Session |
getBottomLeft | 🆗 | driver.execute('flutter:getBottomLeft', buttonFinder) |
Widget |
getBottomRight | 🆗 | driver.execute('flutter:getBottomRight', buttonFinder) |
Widget |
getCenter | 🆗 | driver.execute('flutter:getCenter', buttonFinder) |
Widget |
getRenderObjectDiagnostics | 🆗 | driver.execute('flutter:getRenderObjectDiagnostics', counterTextFinder) |
Widget |
getRenderTree | 🆗 | driver.execute('flutter: getRenderTree') |
Session |
getSemanticsId | 🆗 | driver.execute('flutter:getSemanticsId', counterTextFinder) |
Widget |
getText | 🆗 | driver.getElementText(counterTextFinder) |
Widget |
getTopLeft | 🆗 | driver.execute('flutter:getTopLeft', buttonFinder) |
Widget |
getTopRight | 🆗 | driver.execute('flutter:getTopRight', buttonFinder) |
Widget |
getVmFlags | ❌ | Session | |
getWidgetDiagnostics | ❌ | Widget | |
requestData | 🆗 | driver.execute('flutter:requestData', json.dumps({"deepLink": "myapp://item/id1"})) |
Session |
runUnsynchronized | ❌ | Session | |
setFrameSync | 🆗 | driver.execute('flutter:setFrameSync', bool , durationMilliseconds) |
Session |
screenshot | 🆗 | driver.takeScreenshot() |
Session |
screenshot | 🆗 | driver.saveScreenshot('a.png') |
Session |
scroll | 🆗 | driver.execute('flutter:scroll', find.byType('ListView'), {dx: 50, dy: -100, durationMilliseconds: 200, frequency: 30}) |
Widget |
scrollIntoView | 🆗 | driver.execute('flutter:scrollIntoView', find.byType('TextField'), {alignment: 0.1}) |
Widget |
scrollUntilVisible | 🆗 | driver.execute('flutter:scrollUntilVisible', find.byType('ListView'), {item:find.byType('TextField'), dxScroll: 90, dyScroll: -400}); |
Widget |
setSemantics | ❌ | Session | |
setTextEntryEmulation | 🆗 | driver.execute('flutter:setTextEntryEmulation', false) |
Session |
startTracing | ❌ | Session | |
stopTracingAndDownloadTimeline | ❌ | Session | |
tap | 🆗 | driver.elementClick(buttonFinder) |
Widget |
tap | 🆗 | driver.touchAction({action: 'tap', element: {elementId: buttonFinder}}) |
Widget |
traceAction | ❌ | Session | |
waitFor | 🆗 | driver.execute('flutter:waitFor', buttonFinder, {durationMilliseconds: 100}) |
Widget |
waitForAbsent | 🆗 | driver.execute('flutter:waitForAbsent', buttonFinder) |
Widget |
waitUntilNoTransientCallbacks | ❌ | Widget | |
❓ | 🆗 | setContext |
Appium |
❓ | 🆗 | getCurrentContext |
Appium |
❓ | 🆗 | getContexts |
Appium |
❓ | 🆗 | driver.execute('flutter:longTap', find.byValueKey('increment'), {durationMilliseconds: 10000, frequency: 30}) |
Widget |
❓ | 🆗 | driver.execute('flutter:waitForFirstFrame') |
Widget |
- Flutter context does not support page source
- Please use
getRenderTree
command instead
- Please use
- You can send appium-xcuitest-driver/appium-uiautomator2-driver commands in
NATIVE_APP
context
- CI (unit test / integration test with demo app)
- CD (automatic publish to npm)
-
finder
as a seperate package - switching context between Flutter and AndroidView
- switching context between Flutter and UiKitView
- switching context between Flutter and webview (via UIA2/XCUITest WebView contexts)
- Flutter-version-aware API
- Error handling
$ cd driver
$ npm version <major|minor|patch>
$ git commit -am 'chore: bump version'
$ git tag <version number> # e.g. git tag v0.0.32
$ git push origin v0.0.32
$ npm publish