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 | β | 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 | |
β | β | longTap |
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