Skip to content

Commit 2ffa930

Browse files
feat: Add flutter integration driver commands and tests (#1022)
1 parent bb8d509 commit 2ffa930

26 files changed

+1467
-0
lines changed

.github/workflows/functional-test.yml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,122 @@ jobs:
196196
with:
197197
name: appium-android-${{matrix.test_targets.name}}.log
198198
path: appium.log
199+
200+
flutter_e2e_test:
201+
# These flutter integration driver tests are maintained by: MummanaSubramanya
202+
strategy:
203+
fail-fast: false
204+
matrix:
205+
include:
206+
- platform: macos-14
207+
e2e-tests: flutter-ios
208+
- platform: ubuntu-latest
209+
e2e-tests: flutter-android
210+
211+
runs-on: ${{ matrix.platform }}
212+
213+
env:
214+
API_LEVEL: 28
215+
ARCH: x86
216+
CI: true
217+
XCODE_VERSION: 15.4
218+
IOS_VERSION: 17.5
219+
IPHONE_MODEL: iPhone 15
220+
FLUTTER_ANDROID_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/app-debug.apk"
221+
FLUTTER_IOS_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/ios.zip"
222+
223+
steps:
224+
225+
- uses: actions/checkout@v4
226+
227+
- uses: actions/setup-java@v4
228+
if: matrix.e2e-tests == 'flutter-android'
229+
with:
230+
distribution: 'zulu'
231+
java-version: '17'
232+
233+
- name: Enable KVM group perms
234+
if: matrix.e2e-tests == 'flutter-android'
235+
run: |
236+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
237+
sudo udevadm control --reload-rules
238+
sudo udevadm trigger --name-match=kvm
239+
240+
- name: Set up Python 3.12
241+
uses: actions/setup-python@v3
242+
with:
243+
python-version: 3.12
244+
245+
- name: Install Node.js
246+
uses: actions/setup-node@v4
247+
with:
248+
node-version: 'lts/*'
249+
250+
- name: Install Appium
251+
run: npm install --location=global appium
252+
253+
- name: Install Android drivers and Run Appium
254+
if: matrix.e2e-tests == 'flutter-android'
255+
run: |
256+
appium driver install uiautomator2
257+
appium driver install appium-flutter-integration-driver --source npm
258+
nohup appium --allow-insecure=adb_shell --relaxed-security --log-timestamp --log-no-colors 2>&1 > appium_flutter_android.log &
259+
260+
- name: Run Android tests
261+
if: matrix.e2e-tests == 'flutter-android'
262+
uses: reactivecircus/android-emulator-runner@v2
263+
with:
264+
api-level: ${{ env.API_LEVEL }}
265+
script: |
266+
pip install --upgrade pip
267+
pip install --upgrade pipenv
268+
pipenv lock --clear
269+
pipenv install -d --system
270+
export PLATFORM=android
271+
pytest test/functional/flutter_integration/*_test.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
272+
target: default
273+
disable-spellchecker: true
274+
disable-animations: true
275+
276+
- name: Save server output
277+
if: always() && matrix.e2e-tests == 'flutter-android'
278+
uses: actions/upload-artifact@master
279+
with:
280+
name: appium-flutter-android.log
281+
path: appium_flutter_android.log
282+
283+
- name: Select Xcode
284+
if: matrix.e2e-tests == 'flutter-ios'
285+
uses: maxim-lobanov/setup-xcode@v1
286+
with:
287+
xcode-version: ${{ env.XCODE_VERSION }}
288+
289+
- uses: futureware-tech/simulator-action@v3
290+
if: matrix.e2e-tests == 'flutter-ios'
291+
with:
292+
# https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md
293+
model: ${{ env.IPHONE_MODEL }}
294+
os_version: ${{ env.IOS_VERSION }}
295+
296+
- name: install dependencies
297+
if: matrix.e2e-tests == 'flutter-ios'
298+
run: brew install ffmpeg
299+
300+
- name: Install IOS drivers and Run Appium
301+
if: matrix.e2e-tests == 'flutter-ios'
302+
run: |
303+
appium driver install xcuitest
304+
appium driver install appium-flutter-integration-driver --source npm
305+
appium driver run xcuitest build-wda
306+
nohup appium --allow-insecure=adb_shell --relaxed-security --log-timestamp --log-no-colors 2>&1 > appium_ios.log &
307+
308+
- name: Run IOS tests
309+
if: matrix.e2e-tests == 'flutter-ios'
310+
run: |
311+
# Separate 'run' creates differnet pipenv env. Does them in one run for now.
312+
pip install --upgrade pip
313+
pip install --upgrade pipenv
314+
pipenv lock --clear
315+
pipenv install -d --system
316+
export PLATFORM=ios
317+
pytest test/functional/flutter_integration/*_test.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html

appium/common/helper.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import base64
1516
from typing import Any, Dict
1617

1718
from appium import version as appium_version
@@ -33,3 +34,9 @@ def library_version() -> str:
3334
"""Return a version of this python library"""
3435

3536
return appium_version.version
37+
38+
39+
def encode_file_to_base64(file_path: str) -> str:
40+
"""Return base64 encoded string for given file"""
41+
with open(file_path, 'rb') as file:
42+
return base64.b64encode(file.read()).decode('utf-8')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .base import FlutterOptions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from typing import Dict
19+
20+
from appium.options.common.automation_name_option import AUTOMATION_NAME
21+
from appium.options.common.base import AppiumOptions
22+
from appium.options.flutter_integration.flutter_element_wait_timeout_option import FlutterElementWaitTimeOutOption
23+
from appium.options.flutter_integration.flutter_enable_mock_camera_option import FlutterEnableMockCameraOption
24+
from appium.options.flutter_integration.flutter_server_launch_timeout_option import FlutterServerLaunchTimeOutOption
25+
from appium.options.flutter_integration.flutter_system_port_option import FlutterSystemPortOption
26+
27+
28+
class FlutterOptions(
29+
AppiumOptions,
30+
FlutterElementWaitTimeOutOption,
31+
FlutterEnableMockCameraOption,
32+
FlutterServerLaunchTimeOutOption,
33+
FlutterSystemPortOption,
34+
):
35+
36+
@property
37+
def default_capabilities(self) -> Dict:
38+
return {
39+
AUTOMATION_NAME: 'FlutterIntegration',
40+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from datetime import timedelta
19+
from typing import Optional, Union
20+
21+
from appium.options.common.supports_capabilities import SupportsCapabilities
22+
23+
FLUTTER_ELEMENT_WAIT_TIMEOUT = 'flutterElementWaitTimeout'
24+
25+
26+
class FlutterElementWaitTimeOutOption(SupportsCapabilities):
27+
28+
@property
29+
def flutter_element_wait_timeout(self) -> Optional[timedelta]:
30+
"""
31+
Maximum timeout to wait for element for Flutter integration test
32+
33+
Returns:
34+
Optional[timedelta]: The timeout value as a `timedelta` object if set, or `None` if the timeout is not defined.
35+
"""
36+
return self.get_capability(FLUTTER_ELEMENT_WAIT_TIMEOUT)
37+
38+
@flutter_element_wait_timeout.setter
39+
def flutter_element_wait_timeout(self, value: Union[timedelta, int]) -> None:
40+
"""
41+
Sets the maximum timeout to wait for a Flutter element in an integration test.
42+
Default timeout is 5000ms
43+
44+
Args:
45+
value (Union[timedelta, int]): The timeout value, either as a `timedelta` object or an integer in milliseconds.
46+
If provided as a `timedelta`, it will be converted to milliseconds.
47+
"""
48+
self.set_capability(
49+
FLUTTER_ELEMENT_WAIT_TIMEOUT,
50+
(int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value),
51+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from typing import Optional
19+
20+
from appium.options.common.supports_capabilities import SupportsCapabilities
21+
22+
FLUTTER_ENABLE_MOCK_CAMERA = 'flutterEnableMockCamera'
23+
24+
25+
class FlutterEnableMockCameraOption(SupportsCapabilities):
26+
27+
@property
28+
def flutter_enable_mock_camera(self) -> bool:
29+
"""
30+
Get state of the mock camera for Flutter integration test
31+
32+
Returns:
33+
bool: A boolean indicating whether the mock camera is enabled (True) or disabled (False).
34+
"""
35+
return self.get_capability(FLUTTER_ENABLE_MOCK_CAMERA)
36+
37+
@flutter_enable_mock_camera.setter
38+
def flutter_enable_mock_camera(self, value: bool) -> None:
39+
"""
40+
Setter method enable or disable the mock camera for Flutter integration test
41+
Default state is `False`
42+
43+
Args:
44+
value (bool): A boolean value indicating whether to enable (True) or disable (False) the mock camera.
45+
"""
46+
self.set_capability(FLUTTER_ENABLE_MOCK_CAMERA, value)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from datetime import timedelta
19+
from typing import Optional, Union
20+
21+
from appium.options.common.supports_capabilities import SupportsCapabilities
22+
23+
FLUTTER_SERVER_LAUNCH_TIMEOUT = 'flutterServerLaunchTimeout'
24+
25+
26+
class FlutterServerLaunchTimeOutOption(SupportsCapabilities):
27+
28+
@property
29+
def flutter_server_launch_timeout(self) -> Optional[timedelta]:
30+
"""
31+
Gets the current timeout for launching the Flutter server in a Flutter application.
32+
33+
Returns:
34+
Optional[timedelta]: The timeout value as a `timedelta` object if set, or `None` if the timeout is not defined.
35+
36+
"""
37+
return self.get_capability(FLUTTER_SERVER_LAUNCH_TIMEOUT)
38+
39+
@flutter_server_launch_timeout.setter
40+
def flutter_server_launch_timeout(self, value: Union[timedelta, int]) -> None:
41+
"""
42+
Sets the timeout for launching the Flutter server in Flutter application.
43+
Default timeout is 5000ms
44+
45+
Args:
46+
value (Union[timedelta, int]): The timeout value, either as a `timedelta` object or an integer in milliseconds.
47+
If provided as a `timedelta`, it will be converted to milliseconds.
48+
"""
49+
self.set_capability(
50+
FLUTTER_SERVER_LAUNCH_TIMEOUT,
51+
(int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value),
52+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from typing import Optional
19+
20+
from appium.options.common.supports_capabilities import SupportsCapabilities
21+
22+
FLUTTER_SYSTEM_PORT = 'flutterSystemPort'
23+
24+
25+
class FlutterSystemPortOption(SupportsCapabilities):
26+
27+
@property
28+
def flutter_system_port(self) -> Optional[int]:
29+
"""
30+
Get flutter system port for Flutter integration tests.
31+
32+
Returns:
33+
int: returns the port number
34+
"""
35+
return self.get_capability(FLUTTER_SYSTEM_PORT)
36+
37+
@flutter_system_port.setter
38+
def flutter_system_port(self, value: int) -> None:
39+
"""
40+
Sets the system port for Flutter integration tests.
41+
By default the first free port from 10000..11000 range is selected
42+
43+
Args:
44+
value (int): The port number to be used for the Flutter server.
45+
"""
46+
self.set_capability(FLUTTER_SYSTEM_PORT, value)

appium/webdriver/common/appiumby.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ class AppiumBy(By):
2525
ACCESSIBILITY_ID = 'accessibility id'
2626
IMAGE = '-image'
2727
CUSTOM = '-custom'
28+
29+
# For Flutter integration usage https://github.com/AppiumTestDistribution/appium-flutter-integration-driver/tree/main
30+
FLUTTER_INTEGRATION_SEMANTICS_LABEL = '-flutter semantics label'
31+
FLUTTER_INTEGRATION_TYPE = '-flutter type'
32+
FLUTTER_INTEGRATION_KEY = '-flutter key'
33+
FLUTTER_INTEGRATION_TEXT = '-flutter text'
34+
FLUTTER_INTEGRATION_TEXT_CONTAINING = '-flutter text containing'

0 commit comments

Comments
 (0)