Skip to content

Commit 635e762

Browse files
authored
feat: add method for interacting with the Flutter integration driver (#1123)
1 parent 2680a28 commit 635e762

File tree

2 files changed

+194
-1
lines changed

2 files changed

+194
-1
lines changed

appium/webdriver/extensions/flutter_integration/flutter_commands.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import os
16-
from typing import Any, Dict, Optional, Tuple, Union
16+
from typing import Any, Dict, List, Optional, Tuple, Union
1717

1818
from appium.common.helper import encode_file_to_base64
1919
from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder
@@ -172,6 +172,104 @@ def activate_injected_image(self, image_id: str) -> None:
172172
"""
173173
self.execute_flutter_command('activateInjectedImage', {'imageId': image_id})
174174

175+
def get_render_tree(
176+
self,
177+
widget_type: Optional[str] = None,
178+
key: Optional[str] = None,
179+
text: Optional[str] = None,
180+
) -> List[Optional[Dict]]:
181+
"""
182+
Returns the render tree of the root widget.
183+
184+
Args:
185+
widget_type (Optional[str]): The type of the widget to primary filter by.
186+
key (Optional[str]): The key of the widget to filter by.
187+
text (Optional[str]): The text of the widget to filter by.
188+
189+
Returns:
190+
List[Optional[Dict]]: A list of dictionaries or None values representing the render tree.
191+
192+
The result is a nested list of dictionaries representing each widget and its properties,
193+
such as type, key, size, attribute, state, visual information, and hierarchy.
194+
195+
The example widget includes the following code, which is rendered as part of the widget tree:
196+
```dart
197+
Semantics(
198+
key: const Key('add_activity_semantics'),
199+
label: 'add_activity_button',
200+
button: true,
201+
child: FloatingActionButton.small(
202+
key: const Key('add_activity_button'),
203+
tooltip: 'add_activity_button',
204+
heroTag: 'add',
205+
backgroundColor: const Color(0xFF2E2E3A),
206+
onPressed: null,
207+
child: Icon(
208+
Icons.add,
209+
size: 16,
210+
color: Colors.amber.shade200.withOpacity(0.5),
211+
semanticLabel: 'Add icon',
212+
),
213+
),
214+
),
215+
```
216+
Example execute command:
217+
>>> flutter_command = FlutterCommand(driver) # noqa
218+
>>> flutter_command.get_render_tree(widget_type='Semantics', key='add_activity_semantics')
219+
output >> [
220+
{
221+
"type": "Semantics",
222+
"elementType": "SingleChildRenderObjectElement",
223+
"description": "Semantics-[<'add_activity_semantics'>]",
224+
"depth": 0,
225+
"key": "[<'add_activity_semantics'>]",
226+
"attributes": {
227+
"semanticsLabel": "add_activity_button"
228+
},
229+
"visual": {},
230+
"state": {},
231+
"rect": {
232+
"x": 0,
233+
"y": 0,
234+
"width": 48,
235+
"height": 48
236+
},
237+
"children": [
238+
{
239+
"type": "FloatingActionButton",
240+
"elementType": "StatelessElement",
241+
"description": "FloatingActionButton-[<'add_activity_button'>]",
242+
"depth": 1,
243+
"key": "[<'add_activity_button'>]",
244+
"attributes": {},
245+
"visual": {},
246+
"state": {},
247+
"rect": {
248+
"x": 0,
249+
"y": 0,
250+
"width": 48,
251+
"height": 48
252+
},
253+
"children": [
254+
{...},
255+
"children": [...]
256+
}
257+
]
258+
}
259+
]
260+
}
261+
]
262+
"""
263+
opts = {}
264+
if widget_type is not None:
265+
opts['widgetType'] = widget_type
266+
if key is not None:
267+
opts['key'] = key
268+
if text is not None:
269+
opts['text'] = text
270+
271+
return self.execute_flutter_command('renderTree', opts)
272+
175273
def execute_flutter_command(self, scriptName: str, params: dict) -> Any:
176274
"""
177275
Executes a Flutter command by sending a script and parameters to the flutter integration driver.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
17+
import httpretty
18+
19+
from appium.webdriver.extensions.flutter_integration.flutter_commands import (
20+
FlutterCommand,
21+
)
22+
from test.unit.helper.test_helper import (
23+
appium_command,
24+
flutter_w3c_driver,
25+
get_httpretty_request_body,
26+
)
27+
28+
29+
class TestFlutterRenderTree:
30+
@httpretty.activate
31+
def test_get_render_tree_with_all_filters(self):
32+
expected_body = [{'<partial_tree>': 'LoginButton'}]
33+
httpretty.register_uri(
34+
httpretty.POST,
35+
appium_command('/session/1234567890/execute/sync'),
36+
body=json.dumps({'value': expected_body}),
37+
)
38+
39+
driver = flutter_w3c_driver()
40+
flutter = FlutterCommand(driver)
41+
42+
result = flutter.get_render_tree(widget_type='ElevatedButton', key='LoginButton', text='Login')
43+
44+
request_body = get_httpretty_request_body(httpretty.last_request())
45+
assert request_body['script'] == 'flutter: renderTree'
46+
assert request_body['args'] == [
47+
{
48+
'widgetType': 'ElevatedButton',
49+
'key': 'LoginButton',
50+
'text': 'Login',
51+
}
52+
]
53+
54+
assert result == expected_body
55+
56+
@httpretty.activate
57+
def test_get_render_tree_with_partial_filters(self):
58+
expected_body = [{'<partial_tree>': 'LoginScreen'}]
59+
60+
httpretty.register_uri(
61+
httpretty.POST,
62+
appium_command('/session/1234567890/execute/sync'),
63+
body=json.dumps({'value': expected_body}),
64+
)
65+
66+
driver = flutter_w3c_driver()
67+
flutter = FlutterCommand(driver)
68+
69+
result = flutter.get_render_tree(widget_type='LoginScreen')
70+
71+
request_body = get_httpretty_request_body(httpretty.last_request())
72+
assert request_body['script'] == 'flutter: renderTree'
73+
assert request_body['args'] == [{'widgetType': 'LoginScreen'}]
74+
75+
assert result == expected_body
76+
77+
@httpretty.activate
78+
def test_get_render_tree_with_no_filters(self):
79+
expected_body = [{'<full_tree>': 'RootWidget'}]
80+
httpretty.register_uri(
81+
httpretty.POST,
82+
appium_command('/session/1234567890/execute/sync'),
83+
body=json.dumps({'value': expected_body}),
84+
)
85+
86+
driver = flutter_w3c_driver()
87+
flutter = FlutterCommand(driver)
88+
89+
result = flutter.get_render_tree()
90+
91+
request_body = get_httpretty_request_body(httpretty.last_request())
92+
assert request_body['script'] == 'flutter: renderTree'
93+
assert request_body['args'] == [{}]
94+
95+
assert result == expected_body

0 commit comments

Comments
 (0)